Compare commits

...
Author SHA1 Message Date
tdgao afced420d0 refactor: pnpm prepr 2026-06-07 16:49:01 -07:00
tdgao dff90d0ff9 feat: disable playtime statcard for purely plugin projects 2026-06-07 16:45:54 -07:00
tdgao c1cd2c295e fix: show decimal in playtime statcard if under 1 hrs 2026-06-07 16:45:23 -07:00
tdgao bac27e82df fix: sum rounded value instead of raw value for tooltip total if it's under 1 2026-06-07 16:41:35 -07:00
tdgao 4cb70c13fa feat: fix revenue rounding for values under 1 and show full values for all statcards with tooltip 2026-06-07 15:14:01 -07:00
tdgao 1cf39d9446 feat: add "Your projects" preset selection 2026-06-07 14:55:23 -07:00
tdgao 791f6ee0e0 feat: add button to view user analytics 2026-06-07 14:14:20 -07:00
Calum H.andGitHub 7366c32df3 feat: incompat modal improvement (#6256)
* feat: incompat modal improvement

* feat: use ContentUpdaterModal and remove IncompatibilityWarningModal

* fix: lint

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

* fix: wrong loader icon on server panel

* fix: surface var misalignment

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

* feat: show users on backupitem components

* feat: seperators for filter sections

* fix: lint + change remove -> revoke

* fix: copy

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

* fix: spacing

* feat: qa

* feat: implement backend

* qa: qa pass

* feat: fix user "search"

* fix: lint

* feat: change to bitfield

* feat: fix fields

* fix: lint

* fix: lint

* feat: hook up api

* feat: fix permissions

* feat: audit log table event start

* feat: better mobile mode for audit log table

* feat: i18n

* feat: qa

* feat: enforce permissions

* feat: email template start

* feat: qa

* fix: tooltip bug

* feat: qa

* impl: sse support in api-client

* feat: sse impl

* fix: desync path

* feat: time frame picker from analytics

* feat: QA

* fix: spacing

* fix: permisison audit log entries

* fix: hosting manage page shared server detection

* fix: lint

* feat: qa + lint

* feat: audit log table sort by time

* feat: finish frontend panel stuff

* fix: lint

* fix: backend alignment

* fix: lint

* fix: supress friend errors

* feat: qa

* fix: qa

* fix: lint

* fix: utils barrel

* fix: safari cookies in dev

* fix: pin nuxt

* feat: fixes + notif fix

* fix: notifications

* feat: qa

* fix: notification sync not happening immediately

* fix: qa

* fix: qa

* feat: qa

* blog + prepr

* feat: toast shit

* blog images

* thumbnail update one last time

* prepr

* feat: use reinvite route

* update images

* fix: reinvite stuff

* fix: lint

* fix: alignment of save bar

* fix: notif sizing

* fix: split up access

* fix: lint

* fix: lint

* fix: link

---------

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

* refactor: use sqlx macro

* refactor: better name for escaped query

* style: cleanup unused import

* chore: update sqlx query cache

* fix: tests

---------

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

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

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

* feat: add monetization alert

* fix-small: missing space in tooltip

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

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

* fix: always show month first in chart tooltip

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

* fix: increase length on project name column

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

* fix: double unknown for loader

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

* fix: date picker number inputs showing arrows

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

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

* fix: chips having stroke

* refactor: pnpm prepr

* fix: spacing in annoucement link

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

* fix: reply modal showing up for staff

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

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

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

* fix: fmt

---------

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

* fix: lint + styling

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

* feat: server invite notification type

* feat: direct email notification endpoint

* feat: revoke notification endpoint

* feat: specify users to remove notifications from

* refactor: insert notifications before sending emails

* refactor: rename endpoint

* refactor: remove archon redirect

* style: mark field unused

* feat: dedup external notifications

* feat: add server invite email templates

* style: remove unnecessary format

---------

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

* add env

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

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

* Country filtering

* prepare

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

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

* prepr

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

* feat: use ProgressBar component

* feat: pride skins

* feat: pride skins

* feat: blog post

* fix: blogpost

* fix: pride skin condition

* fix: types

* fix: show logic

* fix: qa

* fix: lint

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

* prepr

* fix: align with backend

* fix: lint

---------

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

* setup webhook and link to user

* fix up code

* improve donation resolution

* Pride 26 campaign

* idempotency

* wip: tiltify

* fix

* redis caching

* add num donators

* fix

* Revert openapi

* Prepare

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

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

* fix: download threshold for projects select

* refactor: pnpm prepr

* feat: handle facets not adding count

* feat: remove getting facets download count field entirely

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

* delete test file
2026-05-29 23:22:08 +02:00
Prospector cef20abceb remove old articles 2026-05-29 13:50:31 -07:00
aecsocketandGitHub 62cedab6dd Analytics faceting fixes (#6243) 2026-05-29 22:46:51 +02:00
Prospector ed5a74a9d3 changelog 2026-05-29 13:03:02 -07:00
Prospector e2318ee776 prepr 2026-05-29 12:57:51 -07:00
Prospector a29c1c61c4 prepr 2026-05-29 12:56:37 -07:00
206813b74c feat: analytics 2.0 blogpost (#6235)
* feat: analytics 2.0 blogpost

* update blog post

* performance: compress mp4s

* date

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: tdgao <mr.trumgao@gmail.com>
2026-05-29 12:47:44 -07:00
aecsocketandGitHub 99611d22c7 Filter out invalid loaders in analytics, fix query bug (#6241)
* should fix bucketing query bug

* Filter out invalid loaders, fix query bug

* fixes
2026-05-29 12:46:59 -07:00
11b2b6e6c0 feat: improve analytics dashboard (#5897)
* feat: implement cancel/apply for custom timeframe range picker

* feat: implement dot for showing todays date

* feat: add max date to be today and show todays date

* feat: if ratio mode, dont show total

* feat: implement show more batching excess lines into "Other" bucket

* refactor: pnpm prepr

* feat: add pick and plop for date range start/end dates

* feat: implement reset query button

* feat: clear button to clear breakdown

* feat: more aggressively trim allowed minimum group by option

* fix: dont show project status filter when from project settings/analytics

* fix: clear selected X above number when appropriate

* feat: graph style updates and dont show year in x axis unless more than 2 year timeframe

* fix: loading state to include legend in blur

* feat: add project icon to project select

* feat: filter out draft projects from analytics

* feat: implement multiselect sections headers, project select org sections, and project options icons

* feat: implement click and drag to select date range

* feat: implement windows history for query builder

* revert: no longer switch breakdown/filter option if same category

* feat: implement showing project for project version breakdown/filter when there are multiple projects

* feat: implement modrinth sided events

* fix: border radius

* feat: implement analytics range highlight

* fix: loading state showing empty state text

* refactor: pnpm prepr

* feat: improve dropdown filter bar and multiselect performance

* fix: multiselect keyboard use

* fix: graph overflow issues

* fix: loading state text on table

* feat: implement tooltip scroll

* fix: adjust charts event tooltip

* feat: shorten time to not repeat am/pm

* feat: implement query params for graph component settings

* fix: qa

* feat: add reset timeframe button

* fix: legend colors moving between metric by determining color based on only downloads metric index

* feat: implement auto switching temporarily to group by day for renvenue metric and disable revenue metric for time range < 2 days

* fix: change to > 1 day

* fix: custom timeframe picker

* feat: implement big performance improvement for table

* feat: implement hover on legend to highlight graph

* fix: defer commit in query builder/filter and style fixes

* feat: more performance optimization to analytics dashboard state, chart, and table

* feat: add tooltip for other item

* feat: improve custom time frame range select

* feat: implement analytics events admin page

* fix: switch column order

* pnpm prepr

* feat: implement mock analytics events

* feat: improve analytics events admin page

* feat: focus title input on analytics create event modal

* fix: remove labels annoying

* feat: hook up analytics events backend

* fix: type error

* feat: reduce combobox padding

* feat: reduce padding on multiselect

* feat: add overlay scrollbar for combobox

* feat: a bunch of style fixes to combobox, multiselect and dropdown filter bar

* feat: MORE PADDING fixes

* feat: use user_agent for download source

* Revert "feat: use user_agent for download source"

This reverts commit d6dc8a99f1.

* fix: query filter project version lag and borked virtualization

* feat: rename breakdown "none" to "project"

* feat: implement right side checkmark for multiselect

* feat: keep crossed out legend items still shown in tooltip but also crossed out

* fix: focus styles

* fix: focus styles pt2

* feat: implement filter by top 8

* fix: preview is incorrect when selecting same date in range date picker

* feat (playtest): cross out legend items in tooltip and allow hide/show in tooltip

* feat (playtest): table component controls what graph shows

* feat: change download source to use user_agent

* feat: fix click to cross out in legend

* feat: add hover legend item to highlight line in tooltip

* fix: export csv to always be dropdown

* feat: implement breakdown = none

* performance: frontend memory reduction

* performance: reduce memory usage from project versions query by keeping only whats necessary

* fix: table checked items not in graph if 0

* feat: add shift click to select a range in table

* performance: add caching for metric types so switching between them is snappy

* performance: batch analytics requests by 15 project ids, with 150 ms delay between, so backend is happy

* feat: add analytics table search

* refactor: pnpm prepr

* fix: query filter options not coming in from analytics fetch

* feat: remove breakdown = none when there are multiple projects

* feat: improve table sorting

* feat: sort projects in project dropdown

* fix: getting project name for project versions

* fix: add loading state for filter and parallel fetch

* performance: use precomputed map for project version options to remove first hover lag

* feat: dropdown filter always open on one side and improve styles

* fix: custom time range picker being weird

* refactor: pnpm prepr

* fix: add back in batch with 300ms interval for projects to prevent backend rate limiting

* performance: only do queries to populate graph first before other analytics queries

* fix: QA polish issues around style and copy

* feat: dont show select all when its just one item in section

* fix: bugs with ratio mode and hiding chart lines

* fix: adjust padding in combobox and multiselect and fix not unfocusing when deselect

* fix: small styles

* fix: polish admin analytic events

* fix: keep scroll position with selection action row appearing when selecting one

* feat: add subheading in graph for showing N items from table

* feat: add unmonetized explaination tooltip

* performance: implement limit on how many lines can be shown in graph

* feat: mobile pass

* refactor: pnpm prepr

* add clear button

* feat: add time in analytics event and normalize date/time so its correct to timezones

* fix: padding

* feat: implement show prev period toggle

* feat: extract TimeFramePicker to packages/ui

* fix: adjust style

* feat: keep table selected persisted in query parameter

* fix: style on prev item value in legend

* fix: when breakdown switches, reset selected series

* fix: tooltip styles

* feat: change project selection to reset to show top 8 only if reconciled down to 0 items

* feat: implement show top 8 button in graph subheading

* fix: rename download type to download reason

* fix: formatting label for table

* feat: persist table sort by and sort direction

* fix: show top 8 button in graph not defaulting to top 8 for other metrics

* feat: implement prev period analytics fetch into the same current period fetch by shifting start date

* refactor: pnpm prepr

* fix: remove number if its just top 1

* fix: brief select items empty state when switch breakdown

* feat: implement format table playtime column

* feat: update export csv filename

* feat: change playtime column to display in hours

* refactor: pnpm prepr

* fix: still download type in filter

* feat: update analytics tooltip

* fix: wrong all projects icon

* feat: force legend order and graph colour for monetization

* refactor: pnpm prepr

* fix: multiselect and combobox sizes

* fix: chart icon add hover delay

* feat: (to playtest) implement multiple breakdowns

* fix: couple UX things for multiple breakdown

* fix: cannot unpin on page click

* fix: multiple breakdown legend and tooltip labels

* feat: add right side checkmark for dropdown filtr bar

* feat: enabling prev period will cross out prev for current ones already crossed out

* feat-mobile: remove drag to select time frame in graph

* feat-mobile: dropdown filter to replace dropdown for submenus on small screen

* feat-mobile: time frame picker to use different start and end date pickers for mobile

* fix-mobile: fix multiselect scroll on mobile

* feat: consolidate is mobile ref into context

* fix-mobile: combobox and multiselect scroll bug when mobile search bar open, fix timeframe picker mobile pick date, and dropdown filter bar click outside to close

* fix-mobile: smaller metric card font

* fix: dropdown filter bar scroll while search

* feat: implement project side events

* feat: implement better mobile view design for query builder

* feat: handle events overflow

* small: add select none

* feat: remove clear project and breakdown

* fix: event icon hover color

* feat: default hide project events if there are multiple projects, and default show if only 1 project

* feat: implement analytics performance updates, including facets, and v3 user projects

* feat: grey out dimmed lined on legend item hover

* feat-mobile: style fixes

* add close on select prop

* feat: add close on select for time frame picker mobile

* feat: date picker default read only

* refactor: pnpm prepr

* feat: default to projects breakdown instead of no breakdown with multiple projects

* fix-mobile: improve graph touch interactions

* small: 2 sig figs on playtime

* feat: deduplicate version uploads that have same version number and are uploaded on same day

* fix: analytics events grouping causing overflow

* feat: improve performance on analytics events grouping

* fix: tooltip expanding page width briefly

* fix: prevent double tap to zoom on inputs

* feat: add click to show chart event for mobile

* fix: toggle not having touch manipulation

* fix: chart tooltip scroll in mobile

* fix: remove project breakdownoption as it is default breakdown when none are selected

* fix: dropdown filter bar briefly empty when switching pages in mobile

* feat: keep tooltip open after drag in mobile

* fix: using plural instead of single for project breakdown

* fix: date picker scrolling page after picking date in mobile by suppressing focus

* fix: callback to Organization instead of org id

* feat: improve chart tooltip date range label formatter to be much more consistent

* feat: tap to toggle event tooltip

* fix: add user select none on graph and fix zoom into download threshold input

* fix: frontend still filtering after backend already filters

* feat: fix emptys state height content shift

* fix: qa issues

* fix: a number of qa issues

- Hide project events based on visible project legend/table selection
- Filter project status events by end status and add explicit copy for approved, private, and unlisted
- Style Modrinth analytics events with blue icon, marker, guide, and range borders
- Add scroll fade shadows to analytics chart and event tooltips
- Show previous-period date range in the chart tooltip
- Make project breakdown conditional on multiple selected projects and allow no breakdown when none are selected
- Add breakdown selection actions and fix “Group by day” copy

* feat: implement graph controls dropdown

* fix date picker typing into time input

* fix: styles in events table

* small: style

* feat: implement using new backend facets route

* feat: implement user get all projects

* performance: deter non-critical fetches to after analytics is in

* fix: refreshing causes multiple projects to do breakdown=none

* performance: cache project version options to fix lag on open sub menu

* refactor: remove chart event height being controlled by parent

* feat: update controls dropdown to have fainter border

* fix: loading bar not fading away

* fix: cannot click in graph

* feat: dont conditionally show multiselect selection actions

* fix: z-index and padding issues

* fix: project events incorrectly toggling on for first page load

* feat: remove show more and show less in legend, always show all

* fix: playtime y axis labels

* feat: improve y axis formatting for playtime and others

* feat: use tabs for game version select, and remove prev period when change breakdown or project selection

* refactor: pnpm prepr

* feat: change hidden legend items to not contribute to ratio percentages

* feat: event icon consume scroll for tooltip panel

* feat: remove gap inside chart tooltip

* feat: add gap for date picker 2 calendar view

* feat: improve analytics events grouping logic for modrinth events to be close to target

* pnpm prepr

* fix: cant click in gap in toggle

* fix: bugs around selected series from table not persisting with timeframe or filter changes

* refactor: kabab case

* refactor: split up large analytics chart and table component files into smaller components and ts modules

* fix: legend is stale after resetting query

* refactor: split up giant analytics provider with utils

* i18n pass

* revert: format number composable change

* fix: playtime was choosing y axis ticks in seconds instead of hours

* refactor: rename folder that with components to match main component name

* refactor: same rename for analytics table for consistency

* refactor: name main components to index.vue and keep folder name as component name

* refactor: pnpm prepr

* refactor: rename types

* refactor: move query builder types into types file and move components into components/analytics-dashboard

* refactor: colocate query builder url with analytics-dashboard component

* refactor: pnpm prepr:frontend

* fix: download threshold not width fit

* fix: no option to see release/all game versions in selected filter dropdown

* fix: game version dropdown width

---------

Signed-off-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-05-29 19:39:55 +00:00
Calum H.andGitHub f49951084e fix: prev message invalid treat as changed (#6236) 2026-05-29 16:19:58 +00:00
Calum H.andGitHub 047b8c3bf7 fix: i18n string problems (#6131)
* fix: apply non-json i18n fixes

* fix: pruning

* fix: prepr

* fix: run.mjs

* fix: lint
2026-05-29 15:55:39 +00:00
Calum H.andGitHub 5c1ffd9ff2 fix: resubmit i18n bug (#6218) 2026-05-29 12:17:08 +00:00
Calum H.andGitHub 670b3c17b6 feat: use split up backup cancel endpoints (#6217) 2026-05-29 12:16:57 +00:00
Prospector ec7c538888 changelog 2026-05-28 18:45:43 -07:00
Prospector b88341e3a8 fix: clean up user page, add verification on modrinth user, fix collection sidebar width 2026-05-28 18:44:25 -07:00
Truman GaoandGitHub 2048d8008a fix: sign in buttons showing in mobile navbar when hidden (#6232) 2026-05-29 01:18:20 +00:00
aecsocketandGitHub 527f2f800b Analytics faceting return count, improve performance (#6229) 2026-05-28 17:58:19 +00:00
SychicandGitHub c7d3229fd3 fix(frontend): incorrect values prop name (#6224) 2026-05-28 16:54:51 +00:00
aecsocketandGitHub ec49a3b051 Move all-projects route to specific user scope (#6220)
* Move all-projects route to specific user scope

* ci

* ci flaky test

* fmt
2026-05-28 14:41:57 +00:00
Calum H.andGitHub cf1d948030 fix: desync on flush (#6223) 2026-05-27 23:22:09 +00:00
fe8fa4b6f7 feat: changelog app 0.14.0 (#6222)
* feat: changelog app 0.14.0

* fix: changelog

* Update packages/blog/changelog.ts

Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Signed-off-by: Calum H. <hendersoncal117@gmail.com>

* Update packages/blog/changelog.ts

Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Signed-off-by: Calum H. <hendersoncal117@gmail.com>

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
2026-05-27 22:47:28 +00:00
Modrinth BotandGitHub 2c62cf1d12 New translations from Crowdin (main) (#6203) 2026-05-27 22:28:26 +00:00
Calum H.andGitHub 84b91f32f8 fix: skins QA problems + flow change (#6216)
* fix: skins backend bugs + apply flow

* fix: caching structure

* feat: collapse already duplicated skins + fix moj api spam

* fix: doc

* fix: flatten migrations

* feat: remove default cape/cape override concept

* fix: fmt + lint

* feat: remove SelectCapeModal for inline cape list

* feat: qa

* feat: virtualisation of skins sections + fix texture/model cache

* fix: lint

* fix: virt bugs + renderer fixes

* fix: qa bugs

* fix: doc

* fix: re-add click impulse anim from prototypes + re-add interact anim length cap

* fix: regressions

* devex: split up SkinPreviewrenderer

* fix: lint

* fix: introduce dynamic mode in virtual-scroll.ts

* feat: qa

* fix: nametag bug + remove minecon skin pack suffix

* feat: pain (literally)

* feat: user agent on moj reqs

* feat: impl per account flush queue for operations

* fix: breadcrumb

* chore: i18n pass

* fix: lint + prep + check

* fix: misalignments
2026-05-27 22:22:24 +00:00
aecsocketandGitHub 64edf2ddeb Improve analytics performance, analytics faceting (#6180)
* fix uri too long

* all projects route for user

* analytics facet fetching

* cache download source regexes

* filtering

* prepare

* Split up analytics metrics into separate modules

* prepare

* fix ci
2026-05-27 00:01:13 +00:00
aecsocketandGitHub 2f248027d6 Also run Daedalus manifest uploads when there's a new game version (#6213) 2026-05-26 21:55:54 +00:00
ThatGravyBoatandGitHub f96520638b feat: Add user-images.githubusercontent.com in allowedHostnames (#6210)
feat: Allow user-images.githubusercontent.com in descriptions
2026-05-26 14:52:35 +00:00
Prospector 9e5d29ced6 hotfix 2026-05-24 12:14:13 -07:00
Prospector 3e15d0b287 changelog 2026-05-24 10:46:35 -07:00
ProspectorandGitHub bcce7e28fd fix: guard against non-strings when decorating download urls (#6194) 2026-05-24 10:42:27 -07:00
ProspectorandGitHub 3889d0f5ec fix: simplify preview banner translation key (#6192) 2026-05-24 10:39:39 -07:00
ProspectorandGitHub 6f44c5b039 feat: add 2 second auto close on skipped notifications (#6191) 2026-05-24 10:39:27 -07:00
Modrinth BotandGitHub ea967845d9 New translations from Crowdin (main) (#6193) 2026-05-24 17:25:25 +00:00
Prospector a6967cf9cb changelog 2026-05-24 09:26:36 -07:00
ProspectorandGitHub 5bf92863b0 fix: spanish locale (#6189) 2026-05-24 09:21:14 -07:00
Prospector 71b6ecc10c fix changelog... 2026-05-23 23:26:34 -07:00
Prospector ed28bc7551 remove april fools redirect 2026-05-23 22:03:59 -07:00
Prospector 3e4197db7c changelog 2026-05-23 22:02:49 -07:00
ProspectorandGitHub e12230ff59 feat: update default memory from 2GB to 4GB (#6172)
* feat: update default memory from 2GB to 4GB

* prepr
2026-05-23 21:26:51 -07:00
ProspectorandGitHub aeb9f5a075 fix: invalid auth cookie causing page not to load (#6186) 2026-05-23 21:26:13 -07:00
Calum H.andGitHub 8b17441f40 feat: compact logs if they have logspam to prevent app crashing (#6181)
* feat: compact logs if they have logspam to prevent app crashing

* fix: lint
2026-05-23 18:22:15 +00:00
f9d47e8edc feat: Add new "Enabled" sorting button next to "Disabled" (#6000)
* feat: Added new "Enabled" sorting button next to "Disabled"

* Updated when filter buttons show

- When all mods are disabled, only "Disabled" shows
- When all mods are enabled, only "Enabled" shows
- When there disabled and enabled mods, then only both buttons show.

* Updated when filter buttons show

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-23 16:12:14 +00:00
a58bc3dc21 feat: java installation ui improvements (#5731)
* Clean impl of java installation ui improvements

* Migrate composable to ts

* Migrate to ButtonStyled, fix coloring

* Fix lint

* Fix clearing java path not refreshing state

* fix: use Table component + install btn disabled state tooltip

---------

Signed-off-by: Arthur <creeperkatze.dev@gmail.com>
Signed-off-by: Arthur <contact@creeperkatze.dev>
Co-authored-by: Creeperkatze <178587183+Creeperkatze@users.noreply.github.com>
Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-05-23 14:46:12 +00:00
ProspectorandGitHub 1e46444fb0 fix: checklist forcing redirects (#6173) 2026-05-22 15:53:22 -07:00
ProspectorandGitHub 1511e55597 fix: skip reviewed projects in queue (#6171) 2026-05-22 14:54:50 -07:00
aecsocketandGitHub 5727e156ed Fetch project analytics events on analytics get (#6143)
* Fetch project analytics events

* fix

* post-query ua bucketing

* fmt
2026-05-22 18:32:33 +00:00
Calum H.andGitHub 657186398d fix: remove broken roadmap reference (#6165)
Signed-off-by: Calum H. <calum@modrinth.com>
2026-05-22 18:10:01 +00:00
Michael H. 3ab2273782 chore: prepr 2026-05-22 19:58:11 +02:00
ProspectorandGitHub 893ec00fc6 feat: add external dep sorting to moderation queue (#6161)
* feat: add external dep sorting to moderation queue

* prepr
2026-05-21 16:40:13 -07:00
Prospector aa7dd1d210 web 2026-05-21 16:23:30 -07:00
Calum H.andGitHub f8733b0488 feat: changelog 21st may 2026 (#6158)
* feat: changelog 21st may 2026

* fix: changelog
2026-05-21 22:27:14 +00:00
Calum H.andGitHub d077d44540 fix: content tab uniqueness regression (#6156)
* fix: content tab uniqueness regression

Closes: #6154

* fix: further regressions

* fix: lint

* fix: lint
2026-05-21 22:03:35 +00:00
aecsocketandGitHub 4e1a61d8b6 Adjust window occlusion threshold (#6157) 2026-05-21 21:48:39 +00:00
aecsocketandGitHub 71dee4de40 Add modpacks with external files filter to moderation queue (#6155)
* simplify query

* make it a tristate

* external deps count
2026-05-21 20:21:54 +00:00
SychicandGitHub f74fad0cae fix: typo in version changelog (#6153) 2026-05-21 19:38:11 +00:00
Truman GaoandGitHub 07e81ac036 fix: project embeds (#6152)
* fix: project embeds

* remove: params.id
2026-05-21 17:02:53 +00:00
Calum H.andGitHub 6e7835fb35 feat: implement kryos upload sessions (#6145)
* feat: implement upload sessions

* fix: files not scoped

* feat: hide staging files folder and proper cancel feedback

* fix: lint
2026-05-21 16:49:48 +00:00
Calum H.andGitHub 2f95c4c441 feat: changelog 20th may 2026 (#6148) 2026-05-20 12:51:36 -07:00
aecsocketandGitHub 451b2d0e44 Window occlusion checks on MacOS (#6135)
* wip: window occlusion checks on MacOS

* wip: occlusion works

* occlusion notification

* fix ci

* fix

* wire in hiding into macos occlusion

* remove debug logs

* fix

* clean up
2026-05-20 19:31:12 +00:00
Truman GaoandGitHub 215643c846 fix: server project type page title (#6147)
* fix: server project type page title

* pnpm prepr
2026-05-20 19:01:19 +00:00
Truman GaoandGitHub d8b1415f9c feat: swap date input to use date picker (#6146)
* feat: swap date input to use date picker

* feat: update date picker to analytics branch changes

* feat: polish date picker usage
2026-05-20 17:50:46 +00:00
Calum H.andGitHub 3eeb549d20 fix: intercom bubble positioning properly (#6111)
* feat: fix intercom properly

* fix: positioning size + css transition

* fix: lint

* fix: ts

* fix: nitpick
2026-05-20 17:15:46 +00:00
Calum H.andGitHub c3fe7b4232 feat: content management changes (#6104)
* feat: change modpack updating flow

* fix: pending install state loss

* fix: mods.vue perf problems

* chore: todo doc

* draft: try preload/fix suspense

* fix: lint
2026-05-20 17:07:35 +00:00
Prospector 079a10bba9 changelog 2026-05-20 09:01:26 -07:00
Prospector 3c3d5702ba fix: permissions page being displayed 2026-05-20 08:59:50 -07:00
Prospector a34576a2c3 changelog 2026-05-19 12:51:59 -07:00
ProspectorandGitHub d8e4915a31 fix: 404 error when changing project slug (#6139) 2026-05-19 19:21:23 +00:00
Prospector ed723fa186 changelog 2026-05-19 11:50:49 -07:00
ProspectorandGitHub d6c8d4475b feat: new north american region info (#6091) 2026-05-19 11:49:41 -07:00
ProspectorandGitHub 302b60d89c fix: random 404s (#6138)
* fix: random 404s when navigating from user, org, or collection page to project

* more

* revert server

* clean names

* prepr
2026-05-19 18:11:48 +00:00
aecsocketandGitHub f106dc580f Restrict what tokens can be used for auth init flows (#6137) 2026-05-19 15:45:58 +00:00
aecsocketandGitHub 244c263e40 Implement analytics marker events (#6090)
* Analytics events

* prepare

* change route prefix

* update route return

* Add mod launcher analytics

* more UA strings

* fix ci

* caching on analytics events

* Return parent modpack versions for playtime queries

* sqlx prepare

* fmt

* dummy fixtures
2026-05-19 13:06:04 +00:00
48bb44155d New translations from Crowdin (main) (#6127)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-18 14:53:39 +00:00
Calum H.andGitHub 3f2e76ae7e fix: table i18n bug (#6129) 2026-05-18 14:53:25 +00:00
6479eca0e9 fix: pix withdrawals (#6128)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-18 14:51:55 +00:00
Calum H.andGitHub 8767bc9184 fix: changelog lint (#6130)
fix: changelog
2026-05-18 14:46:31 +00:00
aecsocketandGitHub d1185414b6 Fix changelog (#6125)
fix changelog
2026-05-17 23:21:10 +02:00
aecsocketandGitHub 510439acbf Changelog (#6124)
changelog
2026-05-17 22:50:23 +02:00
aecsocketandGitHub c564495e11 Ad webview occlusion handling (#6116)
* wip: ad webview occlusion

* Ad webview window occlusion testing

* revert refresh test
2026-05-17 20:27:04 +00:00
simonLeary42andGitHub 8dd1490c8a disable Stable loader version type if there are no stable loader versions available (#6114)
* disable Stable loader version if there are no stable loader versions

* deselect stable if stable is disabled
2026-05-16 17:07:00 +00:00
François-Xavier TalbotandGitHub b72bc18a6b Add moderator notes to users & organizations (#6094)
* Moderator notes

* Use macros

* Improve queries

* Query cache

* Accept missing If-Match if no existing note

* Undo v2 compat changes

* Fix tests

* Remove CONSTRAINT CHECK on moderation_notes

* Respect 1-indexing on moderation_notes.version default in DB migration

* Remove double Option

* .body("") -> .finish()

* .remove() -> .get().clone()

* cloned

* Review comments

* moderation_notes everywhere
2026-05-16 16:30:36 +00:00
aecsocketandGitHub cee942dcef Fix file version updates not considering visibility (#6105) 2026-05-16 16:18:18 +00:00
02a7774722 fix: add download attribute to fix JAR files saving as ZIP in Chromium (#6065)
* fix: add download attribute to fix JAR files saving as ZIP in Chromium

- JAR files were downloading with a `.zip` extension in Chromium-based browsers (Chrome, Edge, Arc, Brave, Opera, Vivaldi)
- Root cause: JAR files are ZIP archives internally, so Chromium sniffs the `Content-Type` as `application/zip` and overrides the filename extension when no `download` attribute is present
- Fix: add `download="<filename>"` to all file download `<a>` tags so the browser uses the original filename from the API

* fix: add download attribute to remaining download links

Missed in initial pass: changelog page button, versions overflow
menu, settings/versions overflow menu. Also adds `download` prop
to Button and OverflowMenu to support dropdown link items.

Adds missing `getPrimaryFile` definition in changelog.vue.

---------

Co-authored-by: Mr_chank <180248271+chank-op@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-15 14:58:26 +00:00
816 changed files with 81107 additions and 17284 deletions
@@ -9,8 +9,6 @@ body:
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate feature requests
required: true
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
required: true
- type: dropdown
id: projects
attributes:
+14
View File
@@ -56,6 +56,17 @@ jobs:
ref: ${{ github.ref }}
token: ${{ secrets.CROWDIN_GH_TOKEN }}
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- name: Enable Corepack
run: corepack enable
- name: Install script dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Configure Git author
id: git-author
uses: MarcoIeni/git-config@59144859caf016f8b817a2ac9b051578729173c4 # v0.1.2
@@ -95,6 +106,9 @@ jobs:
shell: bash
run: sudo chown -R $USER:$USER .
- name: Prune stale ICU translations
run: pnpm scripts i18n-icu-contract prune-local
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
+22 -1
View File
@@ -4,11 +4,14 @@ on:
push:
branches: ['main']
paths:
- '.github/workflows/i18n.push.yml'
- '.github/workflows/i18n-push.yml'
- 'apps/*/src/locales/en-US/**'
- 'apps/*/locales/en-US/**'
- 'packages/*/src/locales/en-US/**'
- 'packages/*/locales/en-US/**'
- 'scripts/i18n-icu-contract.ts'
- 'package.json'
- 'pnpm-lock.yaml'
- 'crowdin.yml'
workflow_dispatch:
@@ -56,6 +59,18 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref }}
fetch-depth: 2
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- name: Enable Corepack
run: corepack enable
- name: Install script dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Query branch name
id: branch-name
@@ -79,3 +94,9 @@ jobs:
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Clear stale ICU translations in Crowdin
run: pnpm scripts i18n-icu-contract clear-crowdin-changed --base-ref HEAD^ --crowdin-branch "[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}"
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+4 -1
View File
@@ -57,8 +57,11 @@ jobs:
- name: Extract app changelog
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ env.VERSION_TAG }}
run: npx --yes tsx scripts/build-theseus-release-notes.ts
run: |
LAST_GITHUB_RELEASE_PUBLISHED_AT=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.published_at // ""' 2>/dev/null || true)
LAST_GITHUB_RELEASE_PUBLISHED_AT="$LAST_GITHUB_RELEASE_PUBLISHED_AT" npx --yes tsx scripts/build-theseus-release-notes.ts
- name: Generate version manifest
run: |
+2 -1
View File
@@ -204,4 +204,5 @@ jobs:
- name: Verify intl:extract has been run
run: |
pnpm turbo run intl:extract --force
git diff --exit-code --color */*/src/locales/en-US/index.json
pnpm scripts i18n-icu-contract prune-local --check
git diff --exit-code --color */*/src/locales/*/index.json
+1 -1
View File
@@ -9,7 +9,7 @@
"files.insertFinalNewline": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "always"
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
+1 -1
View File
@@ -64,7 +64,7 @@ The website and app `prepr` commands
Each project may have its own `CLAUDE.md` with detailed instructions:
- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
## Code Guidelines
Generated
+5
View File
@@ -10514,6 +10514,7 @@ dependencies = [
"flate2",
"fs4",
"futures",
"futures-lite 2.6.1",
"heck 0.5.0",
"hickory-resolver 0.25.2",
"indicatif",
@@ -10565,6 +10566,8 @@ version = "1.0.0-local"
dependencies = [
"async_zip",
"chrono",
"core-foundation 0.10.1",
"core-graphics",
"daedalus",
"dashmap",
"either",
@@ -10572,6 +10575,7 @@ dependencies = [
"hyper 1.7.0",
"hyper-util",
"native-dialog",
"objc2-app-kit",
"paste",
"path-util",
"serde",
@@ -10597,6 +10601,7 @@ dependencies = [
"urlencoding",
"uuid 1.18.1",
"webview2-com",
"windows",
"windows-core 0.61.2",
]
+4
View File
@@ -64,6 +64,8 @@ clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
const_format = "0.2.34"
core-foundation = "0.10.1"
core-graphics = "0.24.0"
daedalus = { path = "packages/daedalus" }
darling = { version = "0.23" }
dashmap = "6.1.0"
@@ -83,6 +85,7 @@ eyre = "0.6.12"
flate2 = "1.1.4"
fs4 = { version = "0.13.1", default-features = false }
futures = "0.3.31"
futures-lite = "2.6.1"
futures-util = "0.3.31"
heck = "0.5.0"
hex = "0.4.3"
@@ -123,6 +126,7 @@ murmur2 = "0.1.0"
native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
notify-debouncer-mini = { version = "0.7.0", default-features = false }
objc2-app-kit = { version = "0.3.2", default-features = false }
p256 = "0.13.2"
parking_lot = "0.12.5"
paste = "1.0.15"
+1 -1
View File
@@ -10,10 +10,10 @@
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/app-frontend",
"test": "vue-tsc --noEmit"
},
"dependencies": {
"@intercom/messenger-js-sdk": "^0.0.14",
"@modrinth/api-client": "workspace:^",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
+178 -101
View File
@@ -1,5 +1,4 @@
<script setup>
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
import {
AuthFeature,
ModrinthApiError,
@@ -37,6 +36,7 @@ import {
ButtonStyled,
commonMessages,
ContentInstallModal,
ContentUpdaterModal,
CreationFlowModal,
defineMessages,
I18nDebugPanel,
@@ -53,6 +53,7 @@ import {
providePopupNotificationManager,
useDebugLogger,
useFormatBytes,
useHostingIntercom,
useVIntl,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
@@ -75,7 +76,6 @@ import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
@@ -84,24 +84,25 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useIntercomPositioning } from '@/composables/intercom-positioning'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { config } from '@/config'
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { get_user, get_version } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack'
import { list } from '@/helpers/profile.js'
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts'
import {
areUpdatesEnabled,
enqueueUpdateForInstallation,
@@ -132,15 +133,40 @@ import { AppPopupNotificationManager } from './providers/app-popup-notifications
const themeStore = useTheming()
const router = useRouter()
const route = useRoute()
const intercomBubblePositioning = useIntercomPositioning({ route, themeStore })
const {
sidebarToggled,
forceSidebar,
sidebarVisible,
intercomBubblePosition,
updateIntercomBubbleStyles,
clearIntercomBubbleStyles,
} = intercomBubblePositioning
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const credentials = ref()
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const prideFundraiserEnabled = computed(
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
)
const hostingIntercomIdentityKey = computed(() => {
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
const userId = credentials.value?.user_id ?? credentials.value?.user?.id ?? 'anonymous'
return `${userId}:${serverId ?? 'hosting'}`
})
const hostingIntercom = useHostingIntercom({
enabled: computed(() => hostingRouteActive.value && !!credentials.value?.session),
appId: 'ykeritl9',
fetchToken: fetchIntercomToken,
identityKey: hostingIntercomIdentityKey,
horizontalPadding: computed(() =>
sidebarVisible.value
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
: INTERCOM_BUBBLE_DEFAULT_PADDING,
),
})
const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
@@ -172,10 +198,20 @@ const tauriApiClient = new TauriModrinthClient({
],
})
provideModrinthClient(tauriApiClient)
const { data: authenticatedModrinthUser } = useQuery({
queryKey: computed(() => ['authenticated-user', 'campaigns', credentials.value?.user?.id]),
queryFn: () => tauriApiClient.labrinth.users_v3.getAuthenticated(),
enabled: () => !!credentials.value?.session,
retry: false,
})
providePageContext({
hierarchicalSidebarAvailable: ref(true),
showAds: ref(false),
...intercomBubblePositioning.pageContext,
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: hostingIntercom.intercomBubble,
featureFlags: {
serverRamAsBytesAlwaysOn: computed(() =>
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
@@ -205,6 +241,7 @@ const {
const news = ref([])
const availableSurvey = ref(false)
const displayedServerInviteNotifications = new Set()
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -259,8 +296,7 @@ onMounted(async () => {
onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
shutdownHostingIntercom()
clearIntercomBubbleStyles()
unsubscribeSidebarToggle()
await unlistenUpdateDownload?.()
})
@@ -307,6 +343,7 @@ async function setupApp() {
locale,
telemetry,
collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
onboarded,
default_page,
@@ -337,6 +374,7 @@ async function setupApp() {
themeStore.setThemeState(theme)
themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering
themeStore.hideNametagSkinsPage = hide_nametag_skins_page
themeStore.toggleSidebar = toggle_sidebar
themeStore.devMode = developer_mode
themeStore.featureFlags = feature_flags
@@ -574,6 +612,16 @@ const {
handleModpackDuplicateCreateAnyway: handleContentInstallModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance: handleContentInstallModpackDuplicateGoToInstance,
setIncompatibilityWarningModal: setContentIncompatibilityWarningModal,
incompatibilityWarningVersions: contentInstallIncompatibilityWarningVersions,
incompatibilityWarningCurrentGameVersion: contentInstallIncompatibilityWarningCurrentGameVersion,
incompatibilityWarningCurrentLoader: contentInstallIncompatibilityWarningCurrentLoader,
incompatibilityWarningProjectType: contentInstallIncompatibilityWarningProjectType,
incompatibilityWarningProjectIconUrl: contentInstallIncompatibilityWarningProjectIconUrl,
incompatibilityWarningProjectName: contentInstallIncompatibilityWarningProjectName,
incompatibilityWarningMessage: contentInstallIncompatibilityWarningMessage,
incompatibilityWarningInstalling: contentInstallIncompatibilityWarningInstalling,
handleIncompatibilityWarningInstall: handleContentInstallIncompatibilityWarningInstall,
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
@@ -593,10 +641,14 @@ const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const updateToPlayModal = ref()
const credentials = ref()
const modrinthLoginFlowWaitModal = ref()
watch(incompatibilityWarningModal, (modal) => {
if (modal) {
setContentIncompatibilityWarningModal(modal)
}
})
setupAuthProvider(credentials, async (_redirectPath) => {
await signIn()
})
@@ -653,21 +705,16 @@ async function logOut() {
await fetchCredentials()
}
const MIDAS_BITFLAG = 1 << 0
const hasPlus = computed(
() =>
credentials.value &&
credentials.value.user &&
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
!!credentials.value?.user &&
(hasMidasBadge(credentials.value.user) ||
hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)),
)
const showAd = computed(
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
let intercomBooting = false
let intercomBooted = false
async function fetchIntercomToken() {
const creds = await getCreds()
@@ -676,8 +723,10 @@ async function fetchIntercomToken() {
}
const params = new URLSearchParams()
if (route.path.startsWith('/hosting/manage/') && typeof route.params.id === 'string') {
params.set('server_id', route.params.id)
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
if (route.path.startsWith('/hosting/manage/') && typeof serverId === 'string') {
params.set('server_id', serverId)
}
const query = params.size > 0 ? `?${params.toString()}` : ''
@@ -693,69 +742,6 @@ async function fetchIntercomToken() {
return await response.json()
}
async function bootIntercom() {
if (
intercomBooting ||
intercomBooted ||
!hostingRouteActive.value ||
!credentials.value?.session
) {
return
}
intercomBooting = true
console.debug('[APP][INTERCOM] initializing secure support chat')
try {
const { token } = await fetchIntercomToken()
Intercom({
app_id: 'ykeritl9',
intercom_user_jwt: token,
session_duration: 1000 * 60 * 60 * 24,
alignment: 'right',
horizontal_padding: intercomBubblePosition.value.horizontalPadding,
vertical_padding: intercomBubblePosition.value.verticalPadding,
})
intercomBooted = true
} catch (error) {
console.warn('[APP][INTERCOM] failed to initialize secure support chat', error)
} finally {
intercomBooting = false
}
}
function shutdownHostingIntercom() {
if (!intercomBooted && !intercomBooting) return
shutdownIntercom()
intercomBooting = false
intercomBooted = false
}
watch(
intercomBubblePosition,
(position) => {
updateIntercomBubbleStyles(position)
if (intercomBooted) {
window.Intercom?.('update', {
horizontal_padding: position.horizontalPadding,
vertical_padding: position.verticalPadding,
})
}
},
{ immediate: true },
)
watch(
[hostingRouteActive, credentials],
([active]) => {
if (active) {
void bootIntercom()
} else {
shutdownHostingIntercom()
}
},
{ immediate: true },
)
watch(showAd, () => {
if (!showAd.value) {
hide_ads_window(true)
@@ -783,6 +769,86 @@ const accounts = ref(null)
provide('accountsCard', accounts)
command_listener(handleCommand)
notification_listener(handleLiveNotification)
async function markLiveNotificationRead(notification) {
try {
await tauriApiClient.labrinth.notifications_v2.markAsRead(notification.id)
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 404) {
console.warn(`notification ${notification.id} could not be marked as read`, error)
return
}
throw error
}
}
async function respondToServerInvite(notification, action) {
const serverId = notification.body?.server_id
if (typeof serverId !== 'string') {
throw new Error('Missing server ID for invite notification.')
}
await tauriApiClient.request(`/servers/${serverId}/invites/${action}`, {
api: 'archon',
version: 1,
method: 'POST',
})
await markLiveNotificationRead(notification)
return serverId
}
async function acceptServerInviteNotification(notification) {
try {
const serverId = await respondToServerInvite(notification, 'accept')
await router.push(`/hosting/manage/${encodeURIComponent(serverId)}`)
queryClient.invalidateQueries({ queryKey: ['servers'] })
} catch (error) {
handleError(error)
}
}
async function declineServerInviteNotification(notification) {
try {
await respondToServerInvite(notification, 'decline')
} catch (error) {
handleError(error)
}
}
function openServerInviteInviterProfile(inviterName) {
if (!inviterName) return
openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`)
}
async function handleLiveNotification(notification) {
if (notification?.body?.type !== 'server_invite' || notification.read) return
if (displayedServerInviteNotifications.has(notification.id)) return
displayedServerInviteNotifications.add(notification.id)
const serverName =
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
const inviterId = notification.body.invited_by
const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
addPopupNotification({
title: serverName,
autoCloseMs: null,
toast: {
type: 'server-invite',
actorName: invitedBy?.username ?? null,
actorAvatarUrl: invitedBy?.avatar_url ?? null,
entityName: serverName,
onAccept: () => acceptServerInviteNotification(notification),
onDecline: () => declineServerInviteNotification(notification),
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
},
})
}
async function handleCommand(e) {
if (!e) return
@@ -1271,7 +1337,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
<CompassIcon />
</NavButton>
<NavButton v-tooltip.right="'Skins (Beta)'" to="/skins">
<NavButton v-tooltip.right="'Skin selector'" to="/skins">
<ChangeSkinIcon />
</NavButton>
<NavButton
@@ -1521,6 +1587,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
</suspense>
</div>
<PrideFundraiserBanner
v-if="prideFundraiserEnabled"
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
/>
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
<div class="space-y-4 flex flex-col items-center w-full">
@@ -1577,7 +1647,22 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@go-to-instance="handleModpackDuplicateGoToInstance"
/>
<AddServerToInstanceModal ref="addServerToInstanceModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<ContentUpdaterModal
ref="incompatibilityWarningModal"
mode="incompatibility-warning"
:versions="contentInstallIncompatibilityWarningVersions"
:current-game-version="contentInstallIncompatibilityWarningCurrentGameVersion"
:current-loader="contentInstallIncompatibilityWarningCurrentLoader"
current-version-id=""
:is-app="true"
:project-type="contentInstallIncompatibilityWarningProjectType"
:project-icon-url="contentInstallIncompatibilityWarningProjectIconUrl"
:project-name="contentInstallIncompatibilityWarningProjectName"
:warning="contentInstallIncompatibilityWarningMessage"
:action-loading="contentInstallIncompatibilityWarningInstalling"
@update="handleContentInstallIncompatibilityWarningInstall"
@cancel="handleContentInstallIncompatibilityWarningCancel"
/>
<ModpackAlreadyInstalledModal
ref="contentInstallModpackAlreadyInstalledModal"
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@@ -1822,14 +1907,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
--os-handle-bg-active: var(--color-scrollbar) !important;
}
.intercom-lightweight-app-launcher,
.intercom-launcher-frame,
iframe[name='intercom-launcher-frame'] {
right: var(--app-support-launcher-right, 20px) !important;
bottom: var(--app-support-launcher-bottom, 20px) !important;
z-index: 9 !important;
}
.mac {
.app-grid-statusbar {
padding-left: 5rem;
@@ -150,7 +150,10 @@ async function refreshValues() {
if (equippedSkin.value) {
try {
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
headUrlCache.value = new Map(headUrlCache.value).set(
equippedSkin.value.texture_key,
headUrl,
)
} catch (error) {
console.warn('Failed to get head render for equipped skin:', error)
}
@@ -160,12 +163,24 @@ async function refreshValues() {
}
}
async function setEquippedSkin(skin: Skin) {
equippedSkin.value = skin
try {
const headUrl = await getPlayerHeadUrl(skin)
headUrlCache.value = new Map(headUrlCache.value).set(skin.texture_key, headUrl)
} catch (error) {
console.warn('Failed to get head render for equipped skin:', error)
}
}
function setLoginDisabled(value: boolean) {
loginDisabled.value = value
}
defineExpose({
refreshValues,
setEquippedSkin,
setLoginDisabled,
loginDisabled,
})
@@ -133,6 +133,7 @@ import {
type PopupNotificationProgressItem,
useVIntl,
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { Dropdown } from 'floating-vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -284,6 +285,7 @@ function goToTerminal(path?: string) {
}
const currentLoadingBars = ref<LoadingBar[]>([])
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
const notificationId = ref<string | number | null>(null)
const dismissed = ref(false)
@@ -303,6 +305,16 @@ function getLoadingText(loadingBar: LoadingBar): string {
return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%`
}
function getDisplayIconUrl(icon: string | null | undefined): string | null {
if (!icon) {
return null
}
if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) {
return icon
}
return convertFileSrc(icon)
}
function getNotification(): PopupNotification | null {
if (!notificationId.value) {
return null
@@ -326,6 +338,7 @@ function buildDownloadItems(): PopupNotificationProgressItem[] {
id: getLoadingBarKey(bar),
title: bar.title ?? '',
text: getLoadingText(bar),
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
progress: getLoadingProgress(bar),
waiting: !bar.total || bar.total <= 0,
}))
@@ -400,6 +413,32 @@ async function refreshLoadingBars() {
.map(formatLoadingBars)
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
const profilePaths = Array.from(
new Set(
currentLoadingBars.value
.map((bar) => bar.bar_type?.profile_path)
.filter((path): path is string => !!path),
),
)
const profiles = profilePaths.length
? await getInstances(profilePaths).catch((error) => {
handleError(error)
return []
})
: []
const profileIconUrls = new Map(
profiles.map((profile) => [profile.path, getDisplayIconUrl(profile.icon_path)]),
)
currentLoadingBarIconUrls.value = Object.fromEntries(
currentLoadingBars.value.map((bar) => {
const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon)
const profileIconUrl = bar.bar_type?.profile_path
? profileIconUrls.get(bar.bar_type.profile_path)
: null
return [getLoadingBarKey(bar), barIconUrl ?? profileIconUrl ?? null]
}),
)
currentLoadingBars.value.sort((a, b) => {
const aKey = `${a.loading_bar_uuid ?? a.id ?? ''}`
const bKey = `${b.loading_bar_uuid ?? b.id ?? ''}`
@@ -1,35 +1,33 @@
<template>
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
<div class="auto-detect-modal">
<div class="table">
<div class="table-row table-head">
<div class="table-cell table-text">Version</div>
<div class="table-cell table-text">Path</div>
<div class="table-cell table-text">Actions</div>
</div>
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
<div class="table-cell table-text">
<span>{{ javaInstall.version }}</span>
</div>
<div v-tooltip="javaInstall.path" class="table-cell table-text">
<span>{{ javaInstall.path }}</span>
</div>
<div class="table-cell table-text manage">
<ButtonStyled v-if="currentSelected.path === javaInstall.path">
<button disabled><CheckIcon /> Selected</button>
<div class="flex flex-col gap-4">
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
<template #cell-version="{ value }">
<span class="font-semibold text-primary">{{ value }}</span>
</template>
<template #cell-path="{ value }">
<span v-tooltip="value" class="block truncate font-mono text-xs">{{ value }}</span>
</template>
<template #cell-actions="{ row }">
<div class="flex items-center justify-end">
<ButtonStyled v-if="currentSelected.path === row.path">
<button class="!shadow-none" disabled><CheckIcon /> Selected</button>
</ButtonStyled>
<ButtonStyled v-else>
<button @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</button>
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
</ButtonStyled>
</div>
</div>
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
<div class="table-cell table-text">No java installations found!</div>
</div>
</div>
<div class="input-group push-right">
</template>
<template #empty-state>
<div class="p-4 text-secondary">No java installations found!</div>
</template>
</Table>
<div class="flex justify-end">
<ButtonStyled type="outlined">
<button @click="$refs.detectJavaModal.hide()">
<button
class="!shadow-none !border-surface-4 !border"
@click="$refs.detectJavaModal.hide()"
>
<XIcon />
Cancel
</button>
@@ -40,7 +38,7 @@
</template>
<script setup>
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
@@ -52,6 +50,11 @@ const { handleError } = injectNotificationManager()
const chosenInstallOptions = ref([])
const detectJavaModal = ref(null)
const currentSelected = ref({})
const javaInstallColumns = [
{ key: 'version', label: 'Version', width: '9rem' },
{ key: 'path', label: 'Path' },
{ key: 'actions', label: 'Actions', align: 'right', width: '10rem' },
]
defineExpose({
show: async (version, currentSelectedJava) => {
@@ -77,25 +80,3 @@ function setJavaInstall(javaInstall) {
})
}
</script>
<style lang="scss" scoped>
.auto-detect-modal {
.table {
.table-row {
grid-template-columns: 1fr 4fr min-content;
}
span {
display: inherit;
align-items: center;
justify-content: center;
}
padding: 0.5rem;
}
}
.manage {
display: flex;
gap: 0.5rem;
}
</style>
@@ -1,85 +1,104 @@
<template>
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
<div class="toggle-setting" :class="{ compact }">
<StyledInput
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
wrapper-class="installation-input"
@update:model-value="
(val) => {
emit('update:modelValue', {
...props.modelValue,
path: val,
})
}
"
/>
<div :id="props.id" class="toggle-setting" :class="{ compact }">
<div class="input-with-status">
<StyledInput
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
wrapper-class="installation-input"
@update:model-value="
(val) => {
emit('update:modelValue', {
...props.modelValue,
path: val,
})
}
"
/>
<ButtonStyled
:color="
!hoveringTest && !testingJava
? testingJavaSuccess === true
? 'green'
: 'red'
: 'standard'
"
color-fill="text"
>
<button
class="!shadow-none"
:disabled="testingJava || props.disabled"
@click="runTest(props.modelValue?.path)"
@mouseenter="!props.disabled && (hoveringTest = true)"
@mouseleave="hoveringTest = false"
>
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
<CheckCircleIcon
v-else-if="testingJavaSuccess === true && !hoveringTest"
class="h-4 w-4"
/>
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
</button>
</ButtonStyled>
</div>
<span class="installation-buttons">
<ButtonStyled v-if="props.version">
<button :disabled="props.disabled || installingJava" @click="reinstallJava">
<button
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
class="!shadow-none"
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
@click="reinstallJava"
>
<DownloadIcon />
{{ installingJava ? 'Installing...' : 'Install recommended' }}
</button>
</ButtonStyled>
<ButtonStyled>
<button :disabled="props.disabled" @click="autoDetect">
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
<SearchIcon />
Detect
</button>
</ButtonStyled>
<ButtonStyled>
<button :disabled="props.disabled" @click="handleJavaFileInput()">
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
<FolderSearchIcon />
Browse
</button>
</ButtonStyled>
<ButtonStyled v-if="testingJava">
<button disabled>Testing...</button>
</ButtonStyled>
<ButtonStyled v-else-if="testingJavaSuccess === true">
<button disabled>
<CheckIcon />
Success
</button>
</ButtonStyled>
<ButtonStyled v-else-if="testingJavaSuccess === false">
<button disabled>
<XIcon />
Failed
</button>
</ButtonStyled>
<ButtonStyled v-else>
<button :disabled="props.disabled" @click="testJava">
<PlayIcon />
Test
</button>
</ButtonStyled>
</span>
</div>
</template>
<script setup>
import {
CheckIcon,
CheckCircleIcon,
DownloadIcon,
FolderSearchIcon,
PlayIcon,
RefreshCwIcon,
SearchIcon,
XIcon,
SpinnerIcon,
XCircleIcon,
} from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref } from 'vue'
import { ref, watch } from 'vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import { trackEvent } from '@/helpers/analytics'
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager()
const props = defineProps({
id: {
type: String,
required: false,
default: null,
},
version: {
type: Number,
required: false,
@@ -110,29 +129,36 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const testingJava = ref(false)
const testingJavaSuccess = ref(null)
const {
testingJava,
javaTestResult: testingJavaSuccess,
testJavaInstallationDebounced,
testJavaInstallation,
} = useJavaTest()
const installingJava = ref(false)
const hoveringTest = ref(false)
let hasInitialized = false
async function testJava() {
testingJava.value = true
testingJavaSuccess.value = await test_jre(
props.modelValue ? props.modelValue.path : '',
props.version,
)
testingJava.value = false
trackEvent('JavaTest', {
path: props.modelValue ? props.modelValue.path : '',
success: testingJavaSuccess.value,
})
setTimeout(() => {
testingJavaSuccess.value = null
}, 2000)
async function runTest(path) {
await testJavaInstallation(path, props.version, true)
}
watch(
() => props.modelValue?.path,
(newPath) => {
if (newPath) {
if (!hasInitialized) {
testJavaInstallation(newPath, props.version, false)
hasInitialized = true
} else {
testJavaInstallationDebounced(newPath, props.version)
}
}
},
{ immediate: true },
)
async function handleJavaFileInput() {
const filePath = await open()
@@ -142,6 +168,7 @@ async function handleJavaFileInput() {
result = {
path: filePath.path ?? filePath,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -175,6 +202,7 @@ async function reinstallJava() {
result = {
path: path,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -186,13 +214,23 @@ async function reinstallJava() {
emit('update:modelValue', result)
installingJava.value = false
runTest(result.path)
}
</script>
<style lang="scss" scoped>
.input-with-status {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
width: 100%;
min-width: 0;
}
.installation-input {
width: 100% !important;
flex-grow: 1;
flex: 1 1 0;
min-width: 0;
}
.toggle-setting {
@@ -215,12 +253,4 @@ async function reinstallJava() {
gap: 0.5rem;
margin: 0;
}
.test-success {
color: var(--color-green);
}
.test-fail {
color: var(--color-red);
}
</style>
@@ -0,0 +1,123 @@
<script setup lang="ts">
import { CalendarIcon, UsersIcon, XIcon } from '@modrinth/assets'
import { injectModrinthClient, ProgressBar } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref } from 'vue'
const DISMISSED_STORAGE_KEY = 'pride-fundraiser-2026-dismissed'
const client = injectModrinthClient()
const dismissed = ref(localStorage.getItem(DISMISSED_STORAGE_KEY) === 'true')
const { data: campaignInfo } = useQuery({
queryKey: ['campaign', 'pride-26'],
queryFn: () => client.labrinth.campaign_internal.getPride26(),
enabled: () => !dismissed.value,
staleTime: 15 * 60 * 1000,
refetchInterval: 15 * 60 * 1000,
retry: false,
})
const shouldShowBanner = computed(
() => !dismissed.value && Number(campaignInfo.value?.target_usd) > 0,
)
async function openPrideFundraiser() {
await openUrl('https://modrinth.com/pride?from=app')
}
function dismissBanner() {
dismissed.value = true
localStorage.setItem(DISMISSED_STORAGE_KEY, 'true')
}
function formatUsd(amount: string | number) {
return Number(amount).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
})
}
function daysLeft() {
return Math.max(
0,
Math.ceil((new Date('2026-07-01T00:00:00Z').getTime() - Date.now()) / (24 * 60 * 60 * 1000)),
)
}
</script>
<template>
<div v-if="shouldShowBanner && campaignInfo">
<section
role="link"
tabindex="0"
class="flex w-full cursor-pointer flex-col gap-3 rounded-xl border border-solid border-surface-5 bg-button-bg p-3 text-primary transition-[border-color,filter] hover:border-surface-6 hover:brightness-125 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
aria-label="Open Pride fundraiser"
@click="openPrideFundraiser"
@keydown.enter="openPrideFundraiser"
@keydown.space.prevent="openPrideFundraiser"
>
<div class="flex w-full items-center justify-between gap-2">
<h2 class="m-0 min-w-0 truncate text-base font-semibold text-contrast">
Pride Fundraiser 2026
</h2>
<button
type="button"
class="m-0 flex size-5 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-primary transition-colors hover:text-contrast focus-visible:text-contrast"
aria-label="Dismiss Pride fundraiser"
@click.stop="dismissBanner"
@keydown.stop
>
<XIcon aria-hidden="true" class="size-5" />
</button>
</div>
<div class="h-px w-full bg-surface-5" />
<div class="flex w-full flex-col gap-2.5">
<div class="flex items-end gap-1 whitespace-nowrap">
<span class="text-base font-semibold leading-5 text-contrast">
{{ formatUsd(campaignInfo.total_donations_usd) }}
</span>
<span class="text-xs font-medium leading-4">
of {{ formatUsd(campaignInfo.target_usd) }}
</span>
</div>
<ProgressBar
class="pride-fundraiser-banner__progress"
:progress="Number(campaignInfo.total_donations_usd)"
:max="Number(campaignInfo.target_usd)"
color="purple"
full-width
:gradient-border="false"
:aria-label="`${formatUsd(campaignInfo.total_donations_usd)} of ${formatUsd(
campaignInfo.target_usd,
)} raised`"
/>
<div class="flex flex-wrap items-center gap-2 text-xs font-medium leading-4">
<span class="flex items-center gap-1">
<UsersIcon aria-hidden="true" class="size-4 shrink-0" />
{{ campaignInfo.num_donators.toLocaleString('en-US') }}
{{ campaignInfo.num_donators === 1 ? 'supporter' : 'supporters' }}
</span>
<span class="flex items-center gap-1">
<CalendarIcon aria-hidden="true" class="size-4 shrink-0" />
{{ daysLeft() }} {{ daysLeft() === 1 ? 'day left' : 'days left' }}
</span>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.pride-fundraiser-banner__progress :deep(.progress-bar) {
background: linear-gradient(
90deg,
var(--color-red) 0%,
var(--color-orange) 20%,
var(--color-green) 50%,
var(--color-blue) 75%,
var(--color-purple) 100%
);
}
</style>
@@ -393,7 +393,6 @@ const messages = defineMessages({
<FriendsSection
v-if="pendingFriends.length > 0"
:is-searching="!!search"
open-by-default
:friends="pendingFriends"
:heading="formatMessage(messages.pending)"
:remove-friend="removeFriend"
@@ -1,185 +0,0 @@
<template>
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
<div class="modal-body">
<p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
installed.
</p>
<table>
<thead>
<tr class="header">
<th>{{ instance?.name }}</th>
<th>{{ project.title }}</th>
</tr>
</thead>
<tbody>
<tr class="content">
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
<td>
<Combobox
v-if="versions?.length > 1"
v-model="selectedVersionId"
:options="versionOptions"
:searchable="true"
placeholder="Select version"
force-direction="up"
:max-height="150"
/>
<span v-else>
<span>{{ selectedVersionLabel }}</span>
</span>
</td>
</tr>
</tbody>
</table>
<div class="button-group">
<ButtonStyled type="outlined">
<button @click="() => incompatibleModal.hide()"><XIcon />Cancel</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="installing" @click="install()">
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
</button>
</ButtonStyled>
</div>
</div>
</ModalWrapper>
</template>
<script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
formatLoader,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { add_project_from_version as installMod } from '@/helpers/profile'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const instance = ref(null)
const project = ref(null)
const versions = ref(null)
const selectedVersion = ref(null)
const incompatibleModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
const selectedVersionLabel = computed(() => {
if (!selectedVersion.value) return ''
return `${selectedVersion.value.name} (${selectedVersion.value.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${selectedVersion.value.game_versions.join(', ')})`
})
const versionOptions = computed(() =>
(versions.value ?? []).map((version) => ({
value: version.id,
label: `${version.name} (${version.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version.game_versions.join(', ')})`,
})),
)
const selectedVersionId = computed({
get: () => selectedVersion.value?.id ?? null,
set: (value) => {
if (!value) return
selectedVersion.value = (versions.value ?? []).find((version) => version.id === value) ?? null
},
})
defineExpose({
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
instance.value = instanceVal
versions.value = projectVersions ?? []
selectedVersion.value = selected ?? projectVersions?.[0] ?? null
project.value = projectVal
onInstall.value = callback
installing.value = false
incompatibleModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
})
const install = async () => {
installing.value = true
await installMod(instance.value.path, selectedVersion.value.id, 'standalone').catch(handleError)
installing.value = false
onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide()
trackEvent('ProjectInstall', {
loader: instance.value.loader,
game_version: instance.value.game_version,
id: project.value,
version_id: selectedVersion.value.id,
project_type: project.value.project_type,
title: project.value.title,
source: 'ProjectIncompatibilityWarningModal',
})
}
</script>
<style lang="scss" scoped>
.data {
text-transform: capitalize;
}
table {
width: 100%;
border-radius: var(--radius-lg);
border-collapse: collapse;
box-shadow: 0 0 0 1px var(--color-button-bg);
}
th {
text-align: left;
padding: 1rem;
background-color: var(--color-bg);
overflow: hidden;
border-bottom: 1px solid var(--color-button-bg);
}
th:first-child {
border-top-left-radius: var(--radius-lg);
border-right: 1px solid var(--color-button-bg);
}
th:last-child {
border-top-right-radius: var(--radius-lg);
}
td {
padding: 1rem;
}
td:first-child {
border-right: 1px solid var(--color-button-bg);
}
.button-group {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
@@ -8,6 +8,7 @@ import {
InstallationSettingsLayout,
provideAppBackup,
provideInstallationSettings,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
@@ -34,8 +35,16 @@ import type { Manifest } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debug = useDebugLogger('AppInstallationSettings')
const { instance, offline, isMinecraftServer, onUnlinked } = injectInstanceSettings()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
debug('metadata load: start', {
instancePath: instance.value.path,
loader: instance.value.loader,
gameVersion: instance.value.game_version,
installStage: instance.value.install_stage,
})
const [
fabric_versions,
@@ -72,6 +81,15 @@ const [
.catch(handleError),
])
debug('metadata load: done', {
hasFabricManifest: !!fabric_versions?.value,
hasForgeManifest: !!forge_versions?.value,
hasQuiltManifest: !!quilt_versions?.value,
hasNeoforgeManifest: !!neoforge_versions?.value,
gameVersions: all_game_versions?.value?.length ?? 0,
availablePlatforms: loaders?.value?.map((loader) => loader.name) ?? [],
})
const { data: modpackInfo } = useQuery({
queryKey: computed(() => ['linkedModpackInfo', instance.value.path]),
queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'),
@@ -95,11 +113,21 @@ function getManifest(loader: string) {
quilt: quilt_versions,
neoforge: neoforge_versions,
}
return map[loader]
const manifest = map[loader]
debug('getManifest:', {
loader,
hasManifest: !!manifest?.value,
gameVersions: manifest?.value?.gameVersions?.length ?? 0,
})
return manifest
}
provideAppBackup({
async createBackup() {
debug('createBackup: start', {
instancePath: instance.value.path,
instanceName: instance.value.name,
})
const allProfiles = await list()
const prefix = `${instance.value.name} - Backup #`
const existingNums = allProfiles
@@ -109,10 +137,12 @@ provideAppBackup({
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
const newPath = await duplicate(instance.value.path)
await edit(newPath, { name: `${prefix}${nextNum}` })
debug('createBackup: done', { newPath, backupName: `${prefix}${nextNum}` })
},
})
provideInstallationSettings({
closeSettings: closeModal,
loading: ref(false),
installationInfo: computed(() => {
const rows = [
@@ -164,32 +194,72 @@ provideInstallationSettings({
const manifest = getManifest(loader)
return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id)
})
return (showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')).map(
(x) => ({ value: x.version, label: x.version }),
)
const result = (
showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')
).map((x) => ({ value: x.version, label: x.version }))
debug('resolveGameVersions:', {
loader,
showSnapshots,
totalVersions: versions.length,
filteredVersions: filtered.length,
resultVersions: result.length,
})
return result
},
resolveLoaderVersions(loader, gameVersion) {
if (loader === 'vanilla' || !gameVersion) return []
const manifest = getManifest(loader)
if (!manifest?.value) return []
if (loader === 'fabric' || loader === 'quilt') {
return manifest.value.gameVersions[0]?.loaders ?? []
if (loader === 'vanilla' || !gameVersion) {
debug('resolveLoaderVersions: skipped', { loader, gameVersion })
return []
}
return manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
const manifest = getManifest(loader)
if (!manifest?.value) {
debug('resolveLoaderVersions: no manifest', { loader, gameVersion })
return []
}
if (loader === 'fabric' || loader === 'quilt') {
const result = manifest.value.gameVersions[0]?.loaders ?? []
debug('resolveLoaderVersions: fabric/quilt result', {
loader,
gameVersion,
count: result.length,
})
return result
}
const result =
manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length })
return result
},
resolveHasSnapshots(loader) {
const versions = all_game_versions?.value ?? []
if (loader === 'vanilla') return versions.some((x) => x.version_type !== 'release')
if (loader === 'vanilla') {
const result = versions.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots: vanilla', { loader, result })
return result
}
const manifest = getManifest(loader)
const supported = versions.filter(
(item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id),
)
return supported.some((x) => x.version_type !== 'release')
const result = supported.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots:', {
loader,
totalVersions: versions.length,
supportedVersions: supported.length,
result,
})
return result
},
async save(platform, gameVersion, loaderVersionId) {
debug('save: called', {
instancePath: instance.value.path,
platform,
gameVersion,
loaderVersionId,
})
const editProfile: Record<string, string | undefined> = {
loader: platform,
game_version: gameVersion,
@@ -198,17 +268,21 @@ provideInstallationSettings({
editProfile.loader_version = loaderVersionId
}
await edit(instance.value.path, editProfile).catch(handleError)
debug('save: edit complete', { editProfile })
},
afterSave: async () => {
debug('afterSave: installing', { instancePath: instance.value.path })
await install(instance.value.path, false).catch(handleError)
trackEvent('InstanceRepair', {
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('afterSave: done')
},
async repair() {
debug('repair: called', { instancePath: instance.value.path })
repairing.value = true
await install(instance.value.path, true).catch(handleError)
repairing.value = false
@@ -216,9 +290,11 @@ provideInstallationSettings({
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('repair: done')
},
async reinstallModpack() {
debug('reinstallModpack: called', { instancePath: instance.value.path })
reinstalling.value = true
await update_repair_modrinth(instance.value.path).catch(handleError)
reinstalling.value = false
@@ -226,9 +302,11 @@ provideInstallationSettings({
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('reinstallModpack: done')
},
async unlinkModpack() {
debug('unlinkModpack: called', { instancePath: instance.value.path })
await edit(instance.value.path, {
linked_data: null as unknown as undefined,
})
@@ -236,27 +314,38 @@ provideInstallationSettings({
queryKey: ['linkedModpackInfo', instance.value.path],
})
onUnlinked()
debug('unlinkModpack: done')
},
getCachedModpackVersions: () => null,
async fetchModpackVersions() {
debug('fetchModpackVersions: called', {
projectId: instance.value.linked_data?.project_id,
})
const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch(
handleError,
)
debug('fetchModpackVersions: done', { count: versions?.length ?? 0 })
return (versions ?? []) as Labrinth.Versions.v2.Version[]
},
async getVersionChangelog(versionId: string) {
debug('getVersionChangelog: called', { versionId })
return (await get_version(versionId, 'must_revalidate').catch(
() => null,
)) as Labrinth.Versions.v2.Version | null
},
async onModpackVersionConfirm(version) {
debug('onModpackVersionConfirm: called', {
versionId: version.id,
instancePath: instance.value.path,
})
await update_managed_modrinth_version(instance.value.path, version.id)
await queryClient.invalidateQueries({
queryKey: ['linkedModpackInfo', instance.value.path],
})
debug('onModpackVersionConfirm: done')
},
updaterModalProps: computed(() => ({
@@ -1,6 +1,15 @@
<script setup lang="ts">
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
import {
CheckCircleIcon,
CoffeeIcon,
FolderSearchIcon,
RefreshCwIcon,
SearchIcon,
SpinnerIcon,
XCircleIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
defineMessages,
injectNotificationManager,
@@ -8,9 +17,11 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, readonly, ref, watch } from 'vue'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
@@ -25,9 +36,54 @@ const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
const overrideJavaInstall = ref(!!instance.value.java_path)
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? instance.value.java_path })
const overrideJavaInstall = ref(!!instance.value.java_path)
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
const activePath = computed(() =>
overrideJavaInstall.value ? javaPath.value : (optimalJava?.path ?? ''),
)
watch(overrideJavaInstall, (enabled) => {
if (enabled && !javaPath.value) {
javaPath.value = optimalJava?.path ?? ''
}
})
const { testingJava, javaTestResult, testJavaInstallationDebounced, testJavaInstallation } =
useJavaTest()
const hoveringTest = ref(false)
let hasInitialized = false
watch(
activePath,
(newPath) => {
if (newPath && optimalJava?.parsed_version) {
if (!hasInitialized) {
testJavaInstallation(newPath, optimalJava?.parsed_version, false)
hasInitialized = true
} else {
testJavaInstallationDebounced(newPath, optimalJava?.parsed_version)
}
}
},
{ immediate: true },
)
const javaDetectionModal = ref<{ show: (version: number, current: object) => void } | null>(null)
async function handleBrowseJava() {
const result = await open({ multiple: false })
if (result) {
javaPath.value = result
}
}
function handleDetectJava() {
javaDetectionModal.value?.show(optimalJava?.parsed_version, { path: javaPath.value })
}
const overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0)
const javaArgs = ref(
@@ -51,8 +107,8 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
const editProfileObject = computed(() => {
return {
java_path:
overrideJavaInstall.value && javaInstall.value.path !== ''
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
overrideJavaInstall.value && javaPath.value
? javaPath.value.replace('java.exe', 'javaw.exe')
: null,
extra_launch_args: overrideJavaArgs.value
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
@@ -71,7 +127,7 @@ const editProfileObject = computed(() => {
watch(
[
overrideJavaInstall,
javaInstall,
javaPath,
overrideJavaArgs,
javaArgs,
overrideEnvVars,
@@ -90,17 +146,45 @@ const messages = defineMessages({
id: 'instance.settings.tabs.java.java-installation',
defaultMessage: 'Java installation',
},
customJavaInstallation: {
id: 'instance.settings.tabs.java.custom-java-installation',
defaultMessage: 'Custom Java installation',
},
javaPathPlaceholder: {
id: 'instance.settings.tabs.java.java-path-placeholder',
defaultMessage: '/path/to/java',
},
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
},
customMemoryAllocation: {
id: 'instance.settings.tabs.java.custom-memory-allocation',
defaultMessage: 'Custom memory allocation',
},
javaArguments: {
id: 'instance.settings.tabs.java.java-arguments',
defaultMessage: 'Java arguments',
},
customJavaArguments: {
id: 'instance.settings.tabs.java.custom-java-arguments',
defaultMessage: 'Custom Java arguments',
},
enterJavaArguments: {
id: 'instance.settings.tabs.java.enter-java-arguments',
defaultMessage: 'Enter Java arguments...',
},
javaEnvironmentVariables: {
id: 'instance.settings.tabs.java.environment-variables',
defaultMessage: 'Environment variables',
},
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
customEnvironmentVariables: {
id: 'instance.settings.tabs.java.custom-environment-variables',
defaultMessage: 'Custom environment variables',
},
enterEnvironmentVariables: {
id: 'instance.settings.tabs.java.enter-environment-variables',
defaultMessage: 'Enter environmental variables...',
},
hooks: {
id: 'instance.settings.tabs.java.hooks',
@@ -111,43 +195,86 @@ const messages = defineMessages({
<template>
<div>
<h2 id="project-name" class="m-0 mb-2.5 text-lg font-semibold text-contrast block">
<JavaDetectionModal ref="javaDetectionModal" @submit="(val) => (javaPath = val.path)" />
<h2 class="m-0 mb-2 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaInstallation) }}
</h2>
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2.5" />
<template v-if="!overrideJavaInstall">
<div class="flex my-2 items-center gap-2 font-semibold">
<template v-if="javaInstall">
<CheckCircleIcon class="text-brand-green h-4 w-4" />
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
</template>
<template v-else-if="optimalJava">
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
one below:</span
<Checkbox
v-model="overrideJavaInstall"
:label="formatMessage(messages.customJavaInstallation)"
class="mb-2"
/>
<div class="flex gap-4 p-4 bg-bg rounded-2xl">
<div class="flex gap-3 items-start flex-1 min-w-0">
<div
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 mt-1 shrink-0 [&_svg]:h-full [&_svg]:w-full"
>
<CoffeeIcon />
</div>
<div class="flex flex-col gap-2 flex-1 min-w-0">
<span class="font-semibold leading-none mt-2"
>Java {{ optimalJava?.parsed_version }}</span
>
</template>
<template v-else>
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not automatically determine a Java installation to use. Please set one
below:</span
>
</template>
<div class="flex gap-2 items-center">
<StyledInput
:model-value="activePath"
:disabled="!overrideJavaInstall"
autocomplete="off"
:placeholder="formatMessage(messages.javaPathPlaceholder)"
wrapper-class="flex-1 min-w-0"
@update:model-value="(val) => (javaPath = String(val))"
/>
<ButtonStyled
:color="
!hoveringTest && !testingJava
? javaTestResult === true
? 'green'
: 'red'
: 'standard'
"
color-fill="text"
>
<button
:disabled="!overrideJavaInstall || testingJava"
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
@mouseenter="overrideJavaInstall && (hoveringTest = true)"
@mouseleave="hoveringTest = false"
>
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
<CheckCircleIcon
v-else-if="javaTestResult === true && !hoveringTest"
class="h-4 w-4"
/>
<XCircleIcon v-else-if="javaTestResult !== true && !hoveringTest" class="h-4 w-4" />
<RefreshCwIcon v-else-if="overrideJavaInstall" class="h-4 w-4" />
</button>
</ButtonStyled>
</div>
<div v-if="overrideJavaInstall" class="flex gap-2">
<ButtonStyled>
<button @click="handleDetectJava">
<SearchIcon />
Detect
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="handleBrowseJava">
<FolderSearchIcon />
Browse
</button>
</ButtonStyled>
</div>
</div>
</div>
<div
v-if="javaInstall && !overrideJavaInstall"
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
>
{{ javaInstall.path }}
</div>
</template>
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
</div>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaMemory) }}
</h2>
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2.5" />
<Checkbox
v-model="overrideMemorySettings"
:label="formatMessage(messages.customMemoryAllocation)"
class="mb-2"
/>
<Slider
id="max-memory"
v-model="memory.maximum"
@@ -159,28 +286,36 @@ const messages = defineMessages({
:snap-range="512"
unit="MB"
/>
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaArguments) }}
</h2>
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
<Checkbox
v-model="overrideJavaArgs"
:label="formatMessage(messages.customJavaArguments)"
class="my-2"
/>
<StyledInput
id="java-args"
v-model="javaArgs"
autocomplete="off"
:disabled="!overrideJavaArgs"
placeholder="Enter java arguments..."
:placeholder="formatMessage(messages.enterJavaArguments)"
wrapper-class="w-full"
/>
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaEnvironmentVariables) }}
</h2>
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2.5" />
<Checkbox
v-model="overrideEnvVars"
:label="formatMessage(messages.customEnvironmentVariables)"
class="mb-2"
/>
<StyledInput
id="env-vars"
v-model="envVars"
autocomplete="off"
:disabled="!overrideEnvVars"
placeholder="Enter environmental variables..."
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
wrapper-class="w-full"
/>
</div>
@@ -45,12 +45,19 @@ const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')
const instanceRef = computed(() => props.instance)
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function hide() {
tabbedModal.value?.hide()
}
provideInstanceSettings({
instance: instanceRef,
offline: props.offline,
isMinecraftServer,
onUnlinked: handleUnlinked,
closeModal: hide,
})
watch(
@@ -113,9 +120,6 @@ const tabs = computed<TabbedModalTab[]>(() => [
},
])
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function show(tabIndex?: number) {
if (props.instance.linked_data?.project_id) {
queryClient.prefetchQuery({
@@ -129,7 +133,7 @@ function show(tabIndex?: number) {
}
}
defineExpose({ show })
defineExpose({ show, hide })
</script>
<template>
<TabbedModal
@@ -10,8 +10,9 @@ import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const worldsInHomeFeatureFlag = 'worlds_in_home' as FeatureFlag
const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
const messages = defineMessages({
colorThemeTitle: {
@@ -100,6 +101,14 @@ const messages = defineMessages({
defaultMessage:
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
},
showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time',
},
showPlayTimeDescription: {
id: 'app.appearance-settings.show-play-time.description',
defaultMessage: `Displays how much time you've spent playing an instance.`,
},
})
const os = ref(await getOS())
@@ -153,16 +162,6 @@ watch(
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
</div>
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
@@ -183,6 +182,44 @@ watch(
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.showPlayTimeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
themeStore.featureFlags[showPlayTimeFlag] = newValue
settings.feature_flags[showPlayTimeFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle
id="hide-nametag-skins-page"
:model-value="themeStore.hideNametagSkinsPage"
@update:model-value="
(e) => {
themeStore.hideNametagSkinsPage = !!e
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
@@ -217,12 +254,12 @@ watch(
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(worldsInHomeFeatureFlag)"
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(worldsInHomeFeatureFlag)
themeStore.featureFlags[worldsInHomeFeatureFlag] = newValue
settings.feature_flags[worldsInHomeFeatureFlag] = newValue
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
themeStore.featureFlags[worldsInHomeFlag] = newValue
settings.feature_flags[worldsInHomeFlag] = newValue
}
"
/>
@@ -236,13 +273,13 @@ watch(
<p class="m-0 mt-1">{{ formatMessage(messages.unknownPackWarningDescription) }}</p>
</div>
<Toggle
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)"
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
@update:model-value="
(e) => {
const warnBeforeUnknownPackInstall = !!e
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFeatureFlag] = skipUnknownPackWarning
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
}
"
/>
@@ -1,149 +1,242 @@
<template>
<UploadSkinModal ref="uploadModal" />
<ModalWrapper ref="modal" @on-hide="resetState">
<NewModal ref="modal" :on-hide="handleModalHide">
<template #title>
<span class="text-lg font-extrabold text-contrast">
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
{{ formatMessage(mode === 'edit' ? messages.editSkinTitle : messages.addSkinTitle) }}
</span>
</template>
<div class="flex flex-col md:flex-row gap-6">
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
<SkinPreviewRenderer
:variant="variant"
:texture-src="previewSkin || ''"
:cape-src="selectedCapeTexture"
:scale="1.4"
:fov="50"
:initial-rotation="Math.PI / 8"
class="h-full w-full"
/>
</div>
<div class="h-[25rem] w-[16rem] min-w-[16rem] flex-shrink-0 md:self-center">
<SkinPreviewRenderer
:variant="variant"
:texture-src="previewSkin || ''"
:cape-src="selectedCapeTexture"
framing="modal"
:initial-rotation="Math.PI / 8"
class="h-full w-full"
/>
</div>
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
<section>
<h2 class="text-base font-semibold mb-2">Texture</h2>
<section v-if="mode === 'edit' && canEditTextureAndModel">
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.textureSection) }}</h2>
<ButtonStyled>
<button @click="openUploadSkinModal"><UploadIcon /> Replace texture</button>
<button class="!shadow-none" @click="openTextureFileBrowser">
<UploadIcon /> {{ formatMessage(messages.replaceTextureButton) }}
</button>
</ButtonStyled>
<input
ref="textureFileInput"
type="file"
accept="image/png"
class="hidden"
@change="onTextureFileInputChange"
/>
</section>
<section>
<h2 class="text-base font-semibold mb-2">Arm style</h2>
<section v-if="canEditTextureAndModel">
<h2 class="text-base font-semibold mb-2">
{{ formatMessage(messages.armStyleSection) }}
</h2>
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
<template #default="{ item }">
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
{{
formatMessage(item === 'CLASSIC' ? messages.wideArmStyle : messages.slimArmStyle)
}}
</template>
</RadioButtons>
</section>
<section>
<h2 class="text-base font-semibold mb-2">Cape</h2>
<div class="flex gap-2">
<CapeButton
v-if="defaultCape"
:id="defaultCape.id"
:texture="defaultCape.texture"
:name="undefined"
:selected="!selectedCape"
faded
@select="selectCape(undefined)"
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.capeSection) }}</h2>
<div class="relative w-fit max-w-full">
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-6"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-6"
leave-to-class="opacity-0 max-h-0"
>
<span>Use default cape</span>
</CapeButton>
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
<span>Use default cape</span>
</CapeLikeTextButton>
<div
v-if="showCapeTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-6 bg-gradient-to-b from-bg-raised to-transparent"
/>
</Transition>
<CapeButton
v-for="cape in visibleCapeList"
:id="cape.id"
:key="cape.id"
:texture="cape.texture"
:name="cape.name || 'Cape'"
:selected="selectedCape?.id === cape.id"
@select="selectCape(cape)"
/>
<CapeLikeTextButton
v-if="(capes?.length ?? 0) > 2"
tooltip="View more capes"
@mouseup="openSelectCapeModal"
<div
ref="capeListRef"
class="grid grid-cols-[repeat(4,max-content)] auto-rows-max gap-2 overflow-y-auto pr-1"
:style="{ maxHeight: capeListMaxHeight }"
@scroll="checkCapeScrollState"
>
<template #icon><ChevronRightIcon /></template>
<span>More</span>
</CapeLikeTextButton>
<CapeLikeTextButton
:tooltip="formatMessage(messages.noCapeTooltip)"
:highlighted="!selectedCape"
@click="selectCape(undefined)"
>
<template #icon><XIcon /></template>
<span>{{ formatMessage(messages.noneCapeOption) }}</span>
</CapeLikeTextButton>
<CapeButton
v-for="cape in sortedCapes"
:id="cape.id"
:key="cape.id"
:texture="cape.texture"
:name="cape.name || formatMessage(messages.capeFallbackName)"
:selected="selectedCape?.id === cape.id"
@select="selectCape(cape)"
/>
</div>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-6"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-6"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showCapeBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-6 bg-gradient-to-t from-bg-raised to-transparent"
/>
</Transition>
</div>
</section>
</div>
</div>
<div class="flex gap-2 mt-12">
<ButtonStyled color="brand">
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
<SpinnerIcon v-if="isSaving" class="animate-spin" />
<CheckIcon v-else-if="mode === 'new'" />
<SaveIcon v-else />
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
</button>
</ButtonStyled>
<ButtonStyled type="outlined">
<button :disabled="isSaving" @click="hide"><XIcon />Cancel</button>
</ButtonStyled>
</div>
</ModalWrapper>
<SelectCapeModal
ref="selectCapeModal"
:capes="capes || []"
@select="handleCapeSelected"
@cancel="handleCapeCancel"
/>
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button :disabled="isSaving" @click="hide">
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
<SpinnerIcon v-if="isSaving" class="animate-spin" />
<CheckIcon v-else-if="mode === 'new'" />
<SaveIcon v-else />
{{ formatMessage(mode === 'new' ? messages.addSkinButton : messages.saveSkinButton) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import {
CheckIcon,
ChevronRightIcon,
SaveIcon,
SpinnerIcon,
UploadIcon,
XIcon,
} from '@modrinth/assets'
import { CheckIcon, SaveIcon, SpinnerIcon, UploadIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
CapeButton,
CapeLikeTextButton,
commonMessages,
defineMessages,
injectNotificationManager,
NewModal,
RadioButtons,
SkinPreviewRenderer,
useScrollIndicator,
useVIntl,
} from '@modrinth/ui'
import { computed, ref, useTemplateRef, watch } from 'vue'
import { arrayBufferToBase64 } from '@modrinth/utils'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
import {
add_and_equip_custom_skin,
type Cape,
determineModelType,
equip_skin,
get_normalized_skin_texture,
remove_custom_skin,
normalize_skin_texture,
save_custom_skin,
type Skin,
type SkinModel,
type SkinTextureUrl,
unequip_skin,
} from '@/helpers/skins.ts'
const CAPE_LIST_MAX_HEIGHT = 334
const messages = defineMessages({
editSkinTitle: {
id: 'app.skins.modal.edit-title',
defaultMessage: 'Editing skin',
},
addSkinTitle: {
id: 'app.skins.modal.add-title',
defaultMessage: 'Adding a skin',
},
textureSection: {
id: 'app.skins.modal.texture-section',
defaultMessage: 'Texture',
},
replaceTextureButton: {
id: 'app.skins.modal.replace-texture-button',
defaultMessage: 'Replace texture',
},
armStyleSection: {
id: 'app.skins.modal.arm-style-section',
defaultMessage: 'Arm style',
},
wideArmStyle: {
id: 'app.skins.modal.arm-style-wide',
defaultMessage: 'Wide',
},
slimArmStyle: {
id: 'app.skins.modal.arm-style-slim',
defaultMessage: 'Slim',
},
capeSection: {
id: 'app.skins.modal.cape-section',
defaultMessage: 'Cape',
},
noCapeTooltip: {
id: 'app.skins.modal.no-cape-tooltip',
defaultMessage: 'No cape',
},
noneCapeOption: {
id: 'app.skins.modal.none-cape-option',
defaultMessage: 'None',
},
capeFallbackName: {
id: 'app.skins.modal.cape-fallback-name',
defaultMessage: 'Cape',
},
savingTooltip: {
id: 'app.skins.modal.saving-tooltip',
defaultMessage: 'Saving...',
},
uploadSkinFirstTooltip: {
id: 'app.skins.modal.upload-skin-first-tooltip',
defaultMessage: 'Upload a skin first!',
},
makeEditFirstTooltip: {
id: 'app.skins.modal.make-edit-first-tooltip',
defaultMessage: 'Make an edit to the skin first!',
},
addSkinButton: {
id: 'app.skins.modal.add-skin-button',
defaultMessage: 'Add skin',
},
saveSkinButton: {
id: 'app.skins.modal.save-skin-button',
defaultMessage: 'Save skin',
},
})
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const modal = useTemplateRef('modal')
const selectCapeModal = useTemplateRef('selectCapeModal')
const textureFileInput = useTemplateRef<HTMLInputElement>('textureFileInput')
const capeListRef = ref<HTMLElement | null>(null)
const capeListMaxHeight = ref(`${CAPE_LIST_MAX_HEIGHT}px`)
const mode = ref<'new' | 'edit'>('new')
const currentSkin = ref<Skin | null>(null)
const shouldRestoreModal = ref(false)
const isSaving = ref(false)
const uploadedTextureUrl = ref<SkinTextureUrl | null>(null)
@@ -151,10 +244,49 @@ const previewSkin = ref<string>('')
const variant = ref<SkinModel>('CLASSIC')
const selectedCape = ref<Cape | undefined>(undefined)
const props = defineProps<{ capes?: Cape[]; defaultCape?: Cape }>()
const props = defineProps<{ capes?: Cape[] }>()
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
const visibleCapeList = ref<Cape[]>([])
const canEditTextureAndModel = computed(() => currentSkin.value?.source !== 'default')
const {
showTopFade: showCapeTopFade,
showBottomFade: showCapeBottomFade,
checkScrollState: checkCapeScrollState,
forceCheck: forceCapeScrollCheck,
} = useScrollIndicator(capeListRef)
let capeListLayoutFrame: number | null = null
function updateCapeListLayout() {
const capeList = capeListRef.value
const modalContent = capeList?.closest('[data-modal-content]') as HTMLElement | null
if (!capeList || !modalContent) {
capeListMaxHeight.value = `${CAPE_LIST_MAX_HEIGHT}px`
forceCapeScrollCheck()
return
}
const availableHeight =
modalContent.getBoundingClientRect().bottom - capeList.getBoundingClientRect().top
capeListMaxHeight.value = `${Math.min(
CAPE_LIST_MAX_HEIGHT,
Math.max(0, Math.floor(availableHeight)),
)}px`
nextTick(() => forceCapeScrollCheck())
}
function refreshCapeListLayout() {
if (capeListLayoutFrame !== null) {
cancelAnimationFrame(capeListLayoutFrame)
}
capeListLayoutFrame = requestAnimationFrame(() => {
capeListLayoutFrame = null
updateCapeListLayout()
})
}
const sortedCapes = computed(() => {
return [...(props.capes || [])].sort((a, b) => {
@@ -164,32 +296,6 @@ const sortedCapes = computed(() => {
})
})
function initVisibleCapeList() {
if (!props.capes || props.capes.length === 0) {
visibleCapeList.value = []
return
}
if (visibleCapeList.value.length === 0) {
if (selectedCape.value) {
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
} else {
visibleCapeList.value = getSortedCapes(2)
}
}
}
function getSortedCapes(count: number): Cape[] {
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
return sortedCapes.value.slice(0, count)
}
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
return sortedCapes.value.find((cape) => cape.id !== excludeId)
}
async function loadPreviewSkin() {
if (uploadedTextureUrl.value) {
previewSkin.value = uploadedTextureUrl.value.normalized
@@ -221,9 +327,13 @@ const disableSave = computed(
)
const saveTooltip = computed(() => {
if (isSaving.value) return 'Saving...'
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
if (isSaving.value) return formatMessage(messages.savingTooltip)
if (mode.value === 'new' && !uploadedTextureUrl.value) {
return formatMessage(messages.uploadSkinFirstTooltip)
}
if (mode.value === 'edit' && !hasEdits.value) {
return formatMessage(messages.makeEditFirstTooltip)
}
return undefined
})
@@ -234,11 +344,13 @@ function resetState() {
previewSkin.value = ''
variant.value = 'CLASSIC'
selectedCape.value = undefined
visibleCapeList.value = []
shouldRestoreModal.value = false
isSaving.value = false
}
function handleModalHide() {
setTimeout(() => resetState(), 250)
}
async function show(e: MouseEvent, skin?: Skin) {
mode.value = skin ? 'edit' : 'new'
currentSkin.value = skin ?? null
@@ -249,12 +361,11 @@ async function show(e: MouseEvent, skin?: Skin) {
variant.value = 'CLASSIC'
selectedCape.value = undefined
}
visibleCapeList.value = []
initVisibleCapeList()
await loadPreviewSkin()
modal.value?.show(e)
nextTick(() => refreshCapeListLayout())
}
async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
@@ -263,98 +374,54 @@ async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
uploadedTextureUrl.value = skinTextureUrl
variant.value = await determineModelType(skinTextureUrl.original)
selectedCape.value = undefined
visibleCapeList.value = []
initVisibleCapeList()
await loadPreviewSkin()
modal.value?.show(e)
nextTick(() => refreshCapeListLayout())
}
async function restoreWithNewTexture(skinTextureUrl: SkinTextureUrl) {
async function setUploadedTexture(skinTextureUrl: SkinTextureUrl) {
uploadedTextureUrl.value = skinTextureUrl
await loadPreviewSkin()
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
}
nextTick(() => refreshCapeListLayout())
}
function hide() {
modal.value?.hide()
setTimeout(() => resetState(), 250)
}
function selectCape(cape: Cape | undefined) {
if (cape && selectedCape.value?.id !== cape.id) {
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
if (!isInVisibleList && visibleCapeList.value.length > 0) {
visibleCapeList.value.splice(0, 1, cape)
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
const otherCape = getSortedCapeExcluding(cape.id)
if (otherCape) {
visibleCapeList.value.splice(1, 1, otherCape)
}
}
}
}
selectedCape.value = cape
}
function handleCapeSelected(cape: Cape | undefined) {
selectCape(cape)
function openTextureFileBrowser() {
textureFileInput.value?.click()
}
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
async function onTextureFileInputChange(e: Event) {
const files = (e.target as HTMLInputElement).files
const file = files?.[0]
if (!file) {
return
}
}
function handleCapeCancel() {
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
}
}
function openSelectCapeModal(e: MouseEvent) {
if (!selectCapeModal.value) return
shouldRestoreModal.value = true
modal.value?.hide()
setTimeout(() => {
selectCapeModal.value?.show(
e,
currentSkin.value?.texture_key,
selectedCape.value,
previewSkin.value,
variant.value,
)
}, 0)
}
function openUploadSkinModal(e: MouseEvent) {
shouldRestoreModal.value = true
modal.value?.hide()
emit('open-upload-modal', e)
}
function restoreModal() {
if (shouldRestoreModal.value) {
setTimeout(() => {
const fakeEvent = new MouseEvent('click')
modal.value?.show(fakeEvent)
shouldRestoreModal.value = false
}, 500)
try {
const originalSkinTexUrl = `data:image/png;base64,${arrayBufferToBase64(
await file.arrayBuffer(),
)}`
const skinTextureNormalized = await normalize_skin_texture(originalSkinTexUrl)
await setUploadedTexture({
original: originalSkinTexUrl,
normalized: `data:image/png;base64,${arrayBufferToBase64(skinTextureNormalized)}`,
})
} catch (error) {
handleError(error)
} finally {
if (textureFileInput.value) {
textureFileInput.value.value = ''
}
}
}
@@ -370,17 +437,32 @@ async function save() {
textureUrl = currentSkin.value!.texture
}
await unequip_skin()
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
if (mode.value === 'new') {
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
emit('saved')
const addedSkin = await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
emit('saved', {
applied: true,
skin: addedSkin,
})
} else {
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
await remove_custom_skin(currentSkin.value!)
emit('saved')
const updatedSkin = await save_custom_skin(
currentSkin.value!,
bytes,
variant.value,
selectedCape.value,
!!uploadedTextureUrl.value && textureUrl !== currentSkin.value?.texture,
)
if (currentSkin.value?.is_equipped) {
await equip_skin(updatedSkin)
}
emit('saved', {
applied: !!currentSkin.value?.is_equipped,
skin: updatedSkin,
previousSkin: currentSkin.value!,
})
}
hide()
@@ -393,28 +475,53 @@ async function save() {
watch([uploadedTextureUrl, currentSkin], async () => {
await loadPreviewSkin()
refreshCapeListLayout()
})
watch(
() => props.capes,
() => {
initVisibleCapeList()
nextTick(() => refreshCapeListLayout())
},
{ immediate: true },
)
watch(
capeListRef,
(capeList, _, onCleanup) => {
if (!capeList) return
const modalContent = capeList.closest('[data-modal-content]')
const resizeObserver = new ResizeObserver(() => refreshCapeListLayout())
if (modalContent instanceof HTMLElement) {
resizeObserver.observe(modalContent)
}
window.addEventListener('resize', refreshCapeListLayout, { passive: true })
refreshCapeListLayout()
onCleanup(() => {
resizeObserver.disconnect()
window.removeEventListener('resize', refreshCapeListLayout)
if (capeListLayoutFrame !== null) {
cancelAnimationFrame(capeListLayoutFrame)
capeListLayoutFrame = null
}
})
},
{ flush: 'post' },
)
const emit = defineEmits<{
(event: 'saved'): void
(event: 'saved', options: { applied: boolean; skin?: Skin; previousSkin?: Skin }): void
(event: 'deleted', skin: Skin): void
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
}>()
defineExpose({
show,
showNew,
restoreWithNewTexture,
hide,
shouldRestoreModal,
restoreModal,
})
</script>
@@ -1,141 +0,0 @@
<script setup lang="ts">
import { CheckIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
CapeButton,
CapeLikeTextButton,
ScrollablePanel,
SkinPreviewRenderer,
} from '@modrinth/ui'
import { computed, ref, useTemplateRef } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import type { Cape, SkinModel } from '@/helpers/skins.ts'
const modal = useTemplateRef('modal')
const emit = defineEmits<{
(e: 'select', cape: Cape | undefined): void
(e: 'cancel'): void
}>()
const props = defineProps<{
capes: Cape[]
}>()
const sortedCapes = computed(() => {
return [...props.capes].sort((a, b) => {
const nameA = (a.name || '').toLowerCase()
const nameB = (b.name || '').toLowerCase()
return nameA.localeCompare(nameB)
})
})
const currentSkinId = ref<string | undefined>()
const currentSkinTexture = ref<string | undefined>()
const currentSkinVariant = ref<SkinModel>('CLASSIC')
const currentCapeTexture = computed<string | undefined>(() => currentCape.value?.texture)
const currentCape = ref<Cape | undefined>()
function show(
e: MouseEvent,
skinId?: string,
selected?: Cape,
skinTexture?: string,
variant?: SkinModel,
) {
currentSkinId.value = skinId
currentSkinTexture.value = skinTexture
currentSkinVariant.value = variant || 'CLASSIC'
currentCape.value = selected
modal.value?.show(e)
}
function select() {
emit('select', currentCape.value)
hide()
}
function hide() {
modal.value?.hide()
emit('cancel')
}
function updateSelectedCape(cape: Cape | undefined) {
currentCape.value = cape
}
function onModalHide() {
emit('cancel')
}
defineExpose({
show,
hide,
})
</script>
<template>
<ModalWrapper ref="modal" @on-hide="onModalHide">
<template #title>
<div class="flex flex-col">
<span class="text-lg font-extrabold text-heading">Change cape</span>
</div>
</template>
<div class="flex flex-col md:flex-row gap-6">
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
<SkinPreviewRenderer
v-if="currentSkinTexture"
:cape-src="currentCapeTexture"
:texture-src="currentSkinTexture"
:variant="currentSkinVariant"
:scale="1.4"
:fov="50"
:initial-rotation="Math.PI + Math.PI / 8"
class="h-full w-full"
/>
</div>
</div>
<div class="flex flex-col gap-4 w-full my-auto">
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
<CapeLikeTextButton
tooltip="No Cape"
:highlighted="!currentCape"
@click="updateSelectedCape(undefined)"
>
<template #icon>
<XIcon />
</template>
<span>None</span>
</CapeLikeTextButton>
<CapeButton
v-for="cape in sortedCapes"
:id="cape.id"
:key="cape.id"
:name="cape.name"
:texture="cape.texture"
:selected="currentCape?.id === cape.id"
@select="updateSelectedCape(cape)"
/>
</div>
</ScrollablePanel>
</div>
</div>
<div class="flex gap-2 items-center">
<ButtonStyled color="brand">
<button @click="select">
<CheckIcon />
Select
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="hide">
<XIcon />
Cancel
</button>
</ButtonStyled>
</div>
</ModalWrapper>
</template>
@@ -1,141 +0,0 @@
<template>
<ModalWrapper ref="modal" @on-hide="hide(true)">
<template #title>
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
</template>
<div class="relative">
<div
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
@click="triggerFileInput"
>
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
<UploadIcon /> Select skin texture file
</p>
<p class="mx-auto mt-0 text-secondary text-sm text-center">
Drag and drop or click here to browse
</p>
<input
ref="fileInput"
type="file"
accept="image/png"
class="hidden"
@change="handleInputFileChange"
/>
</div>
</div>
</ModalWrapper>
</template>
<script setup lang="ts">
import { UploadIcon } from '@modrinth/assets'
import { injectNotificationManager } from '@modrinth/ui'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { onBeforeUnmount, ref, watch } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get_dragged_skin_data } from '@/helpers/skins'
const { addNotification } = injectNotificationManager()
const modal = ref()
const fileInput = ref<HTMLInputElement>()
const unlisten = ref<() => void>()
const modalVisible = ref(false)
const emit = defineEmits<{
(e: 'uploaded', data: ArrayBuffer): void
(e: 'canceled'): void
}>()
function show(e?: MouseEvent) {
modal.value?.show(e)
modalVisible.value = true
setupDragDropListener()
}
function hide(emitCanceled = false) {
modal.value?.hide()
modalVisible.value = false
cleanupDragDropListener()
resetState()
if (emitCanceled) {
emit('canceled')
}
}
function resetState() {
if (fileInput.value) fileInput.value.value = ''
}
function triggerFileInput() {
fileInput.value?.click()
}
async function handleInputFileChange(e: Event) {
const files = (e.target as HTMLInputElement).files
if (!files || files.length === 0) {
return
}
const file = files[0]
const buffer = await file.arrayBuffer()
await processData(buffer)
}
async function setupDragDropListener() {
try {
if (modalVisible.value) {
await cleanupDragDropListener()
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop') {
return
}
if (!event.payload.paths || event.payload.paths.length === 0) {
return
}
const filePath = event.payload.paths[0]
try {
const data = await get_dragged_skin_data(filePath)
await processData(data.buffer)
} catch (error) {
addNotification({
title: 'Error processing file',
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
type: 'error',
})
}
})
}
} catch (error) {
console.error('Failed to set up drag and drop listener:', error)
}
}
async function cleanupDragDropListener() {
if (unlisten.value) {
unlisten.value()
unlisten.value = undefined
}
}
async function processData(buffer: ArrayBuffer) {
emit('uploaded', buffer)
hide()
}
watch(modalVisible, (isVisible) => {
if (isVisible) {
setupDragDropListener()
} else {
cleanupDragDropListener()
}
})
onBeforeUnmount(() => {
cleanupDragDropListener()
})
defineExpose({ show, hide })
</script>
@@ -0,0 +1,432 @@
<script setup lang="ts">
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon, UnknownIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
commonMessages,
defineMessages,
SkinButton,
SkinLikeTextButton,
useScrollViewport,
useVIntl,
} from '@modrinth/ui'
import { useElementSize, useWindowSize } from '@vueuse/core'
import { Tooltip } from 'floating-vue'
import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import type { Skin } from '@/helpers/skins.ts'
type SkinSectionKind = 'saved' | 'default'
type SkinLikeTextButtonExpose = {
getRootElement: () => HTMLElement | null | undefined
}
type AddSkinButtonRef = SkinLikeTextButtonExpose | SkinLikeTextButtonExpose[]
interface DefaultSkinSection {
title: string
infoTooltip?: string
skins: Skin[]
}
interface SkinSection {
key: string
title: string
kind: SkinSectionKind
infoTooltip?: string
skins: Skin[]
}
interface VirtualSkinSection {
section: SkinSection
top: number
index: number
}
const SKIN_CARD_ASPECT_WIDTH = 31
const SKIN_CARD_ASPECT_HEIGHT = 40
const SKIN_GRID_GAP = 12
const SKIN_SECTION_FIRST_SPACING = 4
const SKIN_SECTION_SPACING = 24
const SKIN_SECTION_HEADER_HEIGHT = 28
const SKIN_SECTION_CONTENT_SPACING = 8
const SKIN_SECTION_OVERSCAN = 900
const FALLBACK_CARD_WIDTH = 220
const messages = defineMessages({
savedSkinsSection: {
id: 'app.skins.section.saved-skins',
defaultMessage: 'Saved skins',
},
addSkinButton: {
id: 'app.skins.add-button',
defaultMessage: 'Add skin',
},
dragAndDropSubtitle: {
id: 'app.skins.add-button.drag-and-drop',
defaultMessage: 'Drag and drop',
},
editSkinButton: {
id: 'app.skins.edit-button',
defaultMessage: 'Edit skin',
},
deleteSkinButton: {
id: 'app.skins.delete-button',
defaultMessage: 'Delete skin',
},
})
const props = defineProps<{
savedSkins: Skin[]
defaultSkinSections: DefaultSkinSection[]
getBakedSkinTextures: (skin: Skin) => RenderResult | undefined
isSkinSelected: (skin: Skin) => boolean
isSkinActive: (skin: Skin) => boolean
isAddSkinButtonDragActive: boolean
}>()
const emit = defineEmits<{
select: [skin: Skin]
edit: [skin: Skin, event: MouseEvent]
delete: [skin: Skin]
'add-skin': []
'add-skin-dragenter': [event: DragEvent]
'add-skin-dragover': [event: DragEvent]
'add-skin-dragleave': [event: DragEvent]
'add-skin-drop': [event: DragEvent]
}>()
const addSkinButton = useTemplateRef<AddSkinButtonRef>('addSkinButton')
const { formatMessage } = useVIntl()
const { listContainer, relativeScrollTop, scrollContainer, viewportHeight } = useScrollViewport()
const openSectionKeys = ref<Set<string>>(new Set())
const hasSettledInitialLayout = ref(false)
const knownSectionKeys = new Set<string>()
let enableLayoutTransitionsFrame: number | null = null
let isEnableLayoutTransitionsScheduled = false
let isUnmounted = false
const { width: listWidth } = useElementSize(listContainer)
const { width: windowWidth } = useWindowSize()
const columnCount = computed(() => {
if (windowWidth.value >= 2050) {
return 6
}
if (windowWidth.value >= 1750) {
return 5
}
if (windowWidth.value >= 1300) {
return 4
}
return 3
})
const cardWidth = computed(() => {
if (listWidth.value <= 0) {
return FALLBACK_CARD_WIDTH
}
const gapsWidth = (columnCount.value - 1) * SKIN_GRID_GAP
return Math.max(0, (listWidth.value - gapsWidth) / columnCount.value)
})
const cardHeight = computed(
() => (cardWidth.value * SKIN_CARD_ASPECT_HEIGHT) / SKIN_CARD_ASPECT_WIDTH,
)
const sections = computed<SkinSection[]>(() => [
{
key: 'saved-skins',
title: formatMessage(messages.savedSkinsSection),
kind: 'saved',
skins: props.savedSkins,
},
...props.defaultSkinSections.map((section) => ({
key: defaultSkinSectionKey(section.title),
title: section.title,
kind: 'default' as const,
infoTooltip: section.infoTooltip,
skins: section.skins,
})),
])
const sectionLayouts = computed(() => {
const layouts: Array<{ section: SkinSection; top: number; height: number; index: number }> = []
let top = 0
sections.value.forEach((section, index) => {
const height = getSectionHeightEstimate(section, index)
layouts.push({ section, top, height, index })
top += height
})
return layouts
})
const totalHeight = computed(() => {
const lastSection = sectionLayouts.value[sectionLayouts.value.length - 1]
return lastSection ? lastSection.top + lastSection.height : 0
})
const visibleSections = computed<VirtualSkinSection[]>(() => {
if (!listContainer.value || !scrollContainer.value) {
return sectionLayouts.value.slice(0, 4)
}
const viewportStart = Math.max(0, relativeScrollTop.value - SKIN_SECTION_OVERSCAN)
const viewportEnd = relativeScrollTop.value + viewportHeight.value + SKIN_SECTION_OVERSCAN
return sectionLayouts.value
.filter((layout) => layout.top + layout.height >= viewportStart && layout.top <= viewportEnd)
.map(({ section, top, index }) => ({ section, top, index }))
})
watch(
sections,
(nextSections) => {
const sectionKeys = new Set(nextSections.map((section) => section.key))
const openKeys = new Set(openSectionKeys.value)
for (const section of nextSections) {
if (!knownSectionKeys.has(section.key)) {
knownSectionKeys.add(section.key)
openKeys.add(section.key)
}
}
for (const key of knownSectionKeys) {
if (!sectionKeys.has(key)) {
knownSectionKeys.delete(key)
openKeys.delete(key)
}
}
openSectionKeys.value = openKeys
},
{ immediate: true },
)
watch(
listWidth,
(width) => {
if (
typeof window === 'undefined' ||
width <= 0 ||
hasSettledInitialLayout.value ||
isEnableLayoutTransitionsScheduled
) {
return
}
isEnableLayoutTransitionsScheduled = true
void nextTick(() => {
if (isUnmounted) return
enableLayoutTransitionsFrame = window.requestAnimationFrame(() => {
if (isUnmounted) return
enableLayoutTransitionsFrame = window.requestAnimationFrame(() => {
if (isUnmounted) return
hasSettledInitialLayout.value = true
enableLayoutTransitionsFrame = null
isEnableLayoutTransitionsScheduled = false
})
})
})
},
{ immediate: true },
)
onUnmounted(() => {
isUnmounted = true
if (enableLayoutTransitionsFrame !== null) {
window.cancelAnimationFrame(enableLayoutTransitionsFrame)
}
})
function defaultSkinSectionKey(title: string) {
return `default-skins-${title}`
}
function skinKey(skin: Skin, prefix: string) {
return `${prefix}-${skin.source}-${skin.texture_key}-${skin.variant}-${skin.cape_id ?? 'no-cape'}`
}
function isSectionOpen(key: string) {
return openSectionKeys.value.has(key)
}
function setSectionOpen(key: string, open: boolean) {
const openKeys = new Set(openSectionKeys.value)
if (open) {
openKeys.add(key)
} else {
openKeys.delete(key)
}
openSectionKeys.value = openKeys
}
function getSectionHeightEstimate(section: SkinSection, index: number) {
const spacing = index === 0 ? SKIN_SECTION_FIRST_SPACING : SKIN_SECTION_SPACING
if (!isSectionOpen(section.key)) {
return spacing + SKIN_SECTION_HEADER_HEIGHT
}
const cardCount = section.kind === 'saved' ? section.skins.length + 1 : section.skins.length
const rowCount = Math.ceil(cardCount / columnCount.value)
const gridHeight = rowCount * cardHeight.value + Math.max(0, rowCount - 1) * SKIN_GRID_GAP
return spacing + SKIN_SECTION_HEADER_HEIGHT + SKIN_SECTION_CONTENT_SPACING + gridHeight
}
function getAddSkinButtonElement() {
const button = Array.isArray(addSkinButton.value)
? addSkinButton.value.find((candidate) => candidate.getRootElement())
: addSkinButton.value
return button?.getRootElement()
}
defineExpose({ getAddSkinButtonElement })
</script>
<template>
<div
ref="listContainer"
class="relative w-full"
:style="{ height: `${totalHeight}px`, overflowAnchor: 'none' }"
>
<div
v-for="{ section, top, index } in visibleSections"
:key="section.key"
class="absolute inset-x-0"
:class="[
index === 0 ? 'pt-1' : 'pt-6',
hasSettledInitialLayout
? 'transition-transform duration-300 ease-in-out will-change-transform motion-reduce:transition-none'
: '',
]"
:style="{ transform: `translateY(${top}px)` }"
>
<Accordion
button-class="group flex w-full items-center gap-[6px] bg-transparent m-0 p-0 border-none cursor-pointer text-left"
content-class="pt-2"
:open-by-default="isSectionOpen(section.key)"
@on-open="setSectionOpen(section.key, true)"
@on-close="setSectionOpen(section.key, false)"
>
<template #title>
{{ section.title }}
</template>
<template #button="{ open }">
<DropdownIcon
class="size-6 shrink-0 text-primary transition-transform duration-300"
:class="{ 'rotate-180': open }"
/>
<span class="min-w-0 text-xl font-semibold leading-7 text-primary">
{{ section.title }}
</span>
<Tooltip
v-if="section.infoTooltip"
theme="dismissable-prompt"
placement="top"
:triggers="['hover', 'focus']"
>
<span
class="inline-flex size-6 shrink-0 items-center justify-center text-secondary transition-colors group-hover:text-primary"
@click.stop
>
<UnknownIcon class="size-5" />
</span>
<template #popper>
<p class="m-0 max-w-96 text-wrap text-sm font-medium leading-tight">
{{ section.infoTooltip }}
</p>
</template>
</Tooltip>
</template>
<div
v-if="section.kind === 'saved'"
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
>
<SkinLikeTextButton
ref="addSkinButton"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
dropzone
:drag-active="isAddSkinButtonDragActive"
@click="emit('add-skin')"
@dragenter="emit('add-skin-dragenter', $event)"
@dragover="emit('add-skin-dragover', $event)"
@dragleave="emit('add-skin-dragleave', $event)"
@drop="emit('add-skin-drop', $event)"
>
<template #icon>
<PlusIcon class="size-8" />
</template>
{{ formatMessage(messages.addSkinButton) }}
<template #subtitle>{{ formatMessage(messages.dragAndDropSubtitle) }}</template>
</SkinLikeTextButton>
<SkinButton
v-for="skin in section.skins"
:key="skinKey(skin, 'saved-skin')"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
@select="emit('select', skin)"
>
<template #overlay-buttons>
<ButtonStyled color="brand">
<button
:aria-label="formatMessage(messages.editSkinButton)"
class="pointer-events-auto"
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
>
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
<button
v-tooltip="formatMessage(messages.deleteSkinButton)"
:aria-label="formatMessage(messages.deleteSkinButton)"
class="!rounded-[100%] pointer-events-auto"
@click.stop="emit('delete', skin)"
>
<TrashIcon />
</button>
</ButtonStyled>
</template>
</SkinButton>
</div>
<div
v-else
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
>
<SkinButton
v-for="skin in section.skins"
:key="skinKey(skin, section.key)"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:tooltip="skin.name"
@select="emit('select', skin)"
/>
</div>
</Accordion>
</div>
</div>
</template>
@@ -244,7 +244,7 @@ const messages = defineMessages({
</TagItem>
<div
v-if="world.type === 'singleplayer'"
class="text-sm text-secondary flex items-center gap-1 font-semibold"
class="text-sm text-secondary flex items-center gap-1 font-semibold flex-nowrap whitespace-nowrap"
>
<UserIcon
aria-hidden="true"
@@ -1,121 +0,0 @@
import { computed, onUnmounted, ref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
interface ThemeStore {
toggleSidebar: boolean
$subscribe: (callback: () => void) => () => void
}
interface IntercomBubblePosition {
horizontalPadding: number
verticalPadding: number
}
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const INTERCOM_BUBBLE_WIDTH = 72
const INTERCOM_BUBBLE_RIGHT_VAR = '--app-support-launcher-right'
const INTERCOM_BUBBLE_BOTTOM_VAR = '--app-support-launcher-bottom'
export function useIntercomPositioning({
route,
themeStore,
}: {
route: RouteLocationNormalizedLoaded
themeStore: ThemeStore
}) {
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
onUnmounted(unsubscribeSidebarToggle)
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const defaultIntercomBubbleHorizontalPadding = computed(() =>
sidebarVisible.value
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
: INTERCOM_BUBBLE_DEFAULT_PADDING,
)
const intercomBubbleRequestedHorizontalPadding = ref<number | null>(null)
const intercomBubbleHorizontalPadding = computed(
() =>
intercomBubbleRequestedHorizontalPadding.value ??
defaultIntercomBubbleHorizontalPadding.value,
)
const intercomBubbleVerticalClearance = ref<number | null>(null)
const intercomBubblePosition = computed(() => ({
horizontalPadding: intercomBubbleHorizontalPadding.value,
verticalPadding: intercomBubbleVerticalClearance.value ?? INTERCOM_BUBBLE_DEFAULT_PADDING,
}))
const intercomBubbleHorizontalPaddingRequests = new Map<symbol, number>()
const intercomBubbleClearanceRequests = new Map<symbol, number>()
function requestIntercomBubbleHorizontalPadding(id: symbol, padding: number | null) {
if (padding === null) {
intercomBubbleHorizontalPaddingRequests.delete(id)
} else {
intercomBubbleHorizontalPaddingRequests.set(id, padding)
}
intercomBubbleRequestedHorizontalPadding.value =
intercomBubbleHorizontalPaddingRequests.size > 0
? Math.max(...intercomBubbleHorizontalPaddingRequests.values())
: null
}
function requestIntercomBubbleVerticalClearance(id: symbol, clearance: number | null) {
if (clearance === null) {
intercomBubbleClearanceRequests.delete(id)
} else {
intercomBubbleClearanceRequests.set(id, clearance)
}
intercomBubbleVerticalClearance.value =
intercomBubbleClearanceRequests.size > 0
? Math.max(...intercomBubbleClearanceRequests.values())
: null
}
function updateIntercomBubbleStyles({
horizontalPadding,
verticalPadding,
}: IntercomBubblePosition) {
if (typeof document === 'undefined') return
document.documentElement.style.setProperty(INTERCOM_BUBBLE_RIGHT_VAR, `${horizontalPadding}px`)
document.documentElement.style.setProperty(INTERCOM_BUBBLE_BOTTOM_VAR, `${verticalPadding}px`)
}
function clearIntercomBubbleStyles() {
if (typeof document === 'undefined') return
document.documentElement.style.removeProperty(INTERCOM_BUBBLE_RIGHT_VAR)
document.documentElement.style.removeProperty(INTERCOM_BUBBLE_BOTTOM_VAR)
}
return {
sidebarToggled,
forceSidebar,
sidebarVisible,
intercomBubblePosition,
updateIntercomBubbleStyles,
clearIntercomBubbleStyles,
pageContext: {
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: {
width: ref(INTERCOM_BUBBLE_WIDTH),
horizontalPadding: intercomBubbleHorizontalPadding,
requestHorizontalPadding: requestIntercomBubbleHorizontalPadding,
requestVerticalClearance: requestIntercomBubbleVerticalClearance,
},
},
}
}
@@ -8,7 +8,7 @@ interface LogEntry {
filename: string
name?: string
log_type: string
stdout?: string
output?: string | null
age?: number
live?: boolean
}
@@ -50,12 +50,12 @@ async function getHistoricalLogs(profilePathId: string, instancePath: string): P
const entry = getOrCreate(profilePathId)
if (entry.logList) return entry.logList
const logs: LogEntry[] = await get_logs(instancePath, false)
const logs: LogEntry[] = await get_logs(instancePath, true)
entry.logList = logs
for (const log of logs) {
if (log.stdout && log.stdout !== '') {
entry.historicalCache.set(log.filename, log.stdout)
if (log.output) {
entry.historicalCache.set(log.filename, log.output)
}
}
@@ -0,0 +1,52 @@
import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { test_jre } from '@/helpers/jre.js'
export default function useJavaTest() {
const testingJava = ref(false)
const javaTestResult = ref<boolean | null>(null)
let testDebounceTimer: ReturnType<typeof setTimeout> | null = null
async function runJavaTest(path: string, version: number, track = true) {
if (testDebounceTimer) {
clearTimeout(testDebounceTimer)
testDebounceTimer = null
}
if (!path) {
javaTestResult.value = null
return
}
testingJava.value = true
try {
javaTestResult.value = await test_jre(path, version)
} catch {
javaTestResult.value = false
}
testingJava.value = false
if (track) {
trackEvent('JavaTest', { path, success: javaTestResult.value })
}
}
function testJavaInstallationDebounced(path: string, version: number, delay = 600) {
if (testDebounceTimer) clearTimeout(testDebounceTimer)
if (!path) {
javaTestResult.value = null
return
}
testDebounceTimer = setTimeout(() => runJavaTest(path, version, false), delay)
}
async function testJavaInstallation(path: string, version: number, track = false) {
await runJavaTest(path, version, track)
}
return {
testingJava,
javaTestResult,
testJavaInstallationDebounced,
testJavaInstallation,
}
}
+4
View File
@@ -98,6 +98,10 @@ export async function friend_listener(callback) {
return await listen('friend', (event) => callback(event.payload))
}
export async function notification_listener(callback) {
return await listen('notification', (event) => callback(event.payload))
}
/// Payload for the 'log' event
/*
LogPayload {
@@ -0,0 +1,100 @@
import type {
ContentItem,
ContentModpackCardCategory,
ContentModpackCardProject,
ContentModpackCardVersion,
ContentOwner,
} from '@modrinth/ui'
import {
get_content_items,
get_linked_modpack_info,
type LinkedModpackInfo,
} from '@/helpers/profile'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour } from '@/helpers/types'
export type InstanceContentData = {
path: string
contentItems: ContentItem[] | null
modpack: InstanceContentModpackData | null
}
export type InstanceContentModpackData = {
project: ContentModpackCardProject
version: ContentModpackCardVersion
owner: ContentOwner | null
categories: ContentModpackCardCategory[]
hasUpdate: boolean
updateVersionId: string | null
}
export async function loadInstanceContentData(
path: string,
cacheBehaviour?: CacheBehaviour,
onError?: (error: Error) => unknown,
): Promise<InstanceContentData> {
const [contentItems, modpackInfo, allCategories] = await Promise.all([
get_content_items(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_linked_modpack_info(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_categories().catch((error) => handleLoadError(error, onError)),
])
return {
path,
contentItems: (contentItems as ContentItem[] | null | undefined) ?? null,
modpack: normalizeLinkedModpackInfo(
modpackInfo as LinkedModpackInfo | null | undefined,
allCategories as ContentModpackCardCategory[] | null | undefined,
),
}
}
function handleLoadError(error: unknown, onError?: (error: Error) => unknown) {
onError?.(error as Error)
return null
}
function normalizeLinkedModpackInfo(
modpackInfo: LinkedModpackInfo | null | undefined,
allCategories: ContentModpackCardCategory[] | null | undefined,
): InstanceContentModpackData | null {
if (!modpackInfo) return null
return {
project: {
...modpackInfo.project,
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
icon_url: modpackInfo.project.icon_url ?? undefined,
},
version: {
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
},
owner: modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null,
categories: resolveLinkedModpackCategories(modpackInfo, allCategories),
hasUpdate: modpackInfo.has_update,
updateVersionId: modpackInfo.update_version_id,
}
}
function resolveLinkedModpackCategories(
modpackInfo: LinkedModpackInfo,
allCategories: ContentModpackCardCategory[] | null | undefined,
) {
if (!allCategories || !modpackInfo.project.categories) return []
const seen = new Set<string>()
return allCategories.filter((category) => {
if (modpackInfo.project.categories.includes(category.name) && !seen.has(category.name)) {
seen.add(category.name)
return true
}
return false
})
}
+2 -3
View File
@@ -6,12 +6,11 @@
import { invoke } from '@tauri-apps/api/core'
/*
A log is a struct containing the filename string, stdout, and stderr, as follows:
A log is a struct containing the filename string and optional output, as follows:
pub struct Logs {
pub filename: String,
pub stdout: String,
pub stderr: String,
pub output: Option<String>,
}
*/
+8 -1
View File
@@ -147,9 +147,16 @@ export async function get_mod_full_path(path: string, projectPath: string): Prom
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
}
export interface JavaVersion {
parsed_version: number
version: string
architecture: string
path: string
}
// Get optimal java version from profile
// Returns a java version
export async function get_optimal_jre_key(path: string): Promise<string | null> {
export async function get_optimal_jre_key(path: string): Promise<JavaVersion | null> {
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
}
@@ -3,9 +3,8 @@ import {
applyCapeTexture,
createTransparentTexture,
disposeCaches,
loadTexture,
setupSkinModel,
} from '@modrinth/utils'
} from '@modrinth/ui'
import * as THREE from 'three'
import { reactive } from 'vue'
@@ -29,6 +28,7 @@ class BatchSkinRenderer {
private scene: THREE.Scene | null = null
private camera: THREE.PerspectiveCamera | null = null
private currentModel: THREE.Group | null = null
private transparentTexture: THREE.Texture | null = null
private readonly width: number
private readonly height: number
@@ -52,6 +52,7 @@ class BatchSkinRenderer {
})
this.renderer.outputColorSpace = THREE.SRGBColorSpace
this.renderer.shadowMap.enabled = false
this.renderer.toneMapping = THREE.NoToneMapping
this.renderer.toneMappingExposure = 10.0
this.renderer.setClearColor(0x000000, 0)
@@ -62,7 +63,7 @@ class BatchSkinRenderer {
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
directionalLight.castShadow = true
directionalLight.castShadow = false
directionalLight.position.set(2, 4, 3)
this.scene.add(ambientLight)
this.scene.add(directionalLight)
@@ -112,9 +113,19 @@ class BatchSkinRenderer {
this.renderer.render(this.scene, this.camera)
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
const response = await fetch(dataUrl)
return await response.blob()
return await new Promise<Blob>((resolve, reject) => {
this.renderer!.domElement.toBlob(
(blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create blob from rendered canvas'))
}
},
'image/webp',
0.9,
)
})
}
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
@@ -122,14 +133,10 @@ class BatchSkinRenderer {
throw new Error('Renderer not initialized')
}
const { model } = await setupSkinModel(modelUrl, textureUrl)
const { model } = await setupSkinModel(modelUrl, textureUrl, capeUrl)
if (capeUrl) {
const capeTexture = await loadTexture(capeUrl)
applyCapeTexture(model, capeTexture)
} else {
const transparentTexture = createTransparentTexture()
applyCapeTexture(model, null, transparentTexture)
if (!capeUrl) {
applyCapeTexture(model, null, this.getTransparentTexture())
}
const group = new THREE.Group()
@@ -141,39 +148,38 @@ class BatchSkinRenderer {
this.currentModel = group
}
private clearScene(): void {
if (!this.scene) return
while (this.scene.children.length > 0) {
const child = this.scene.children[0]
this.scene.remove(child)
if (child instanceof THREE.Mesh) {
if (child.geometry) child.geometry.dispose()
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach((material) => material.dispose())
} else {
child.material.dispose()
}
}
}
private getTransparentTexture(): THREE.Texture {
if (!this.transparentTexture) {
this.transparentTexture = createTransparentTexture()
}
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
directionalLight.castShadow = true
directionalLight.position.set(2, 4, 3)
this.scene.add(ambientLight)
this.scene.add(directionalLight)
return this.transparentTexture
}
private clearScene(): void {
if (!this.scene || !this.currentModel) return
this.scene.remove(this.currentModel)
this.currentModel.clear()
this.currentModel = null
}
public dispose(): void {
this.clearScene()
if (this.transparentTexture) {
this.transparentTexture.dispose()
this.transparentTexture = null
}
if (this.renderer) {
this.renderer.dispose()
}
this.renderer = null
this.scene = null
this.camera = null
disposeCaches()
}
}
@@ -194,6 +200,9 @@ export const headBlobUrlMap = reactive(new Map<string, string>())
const DEBUG_MODE = false
let sharedRenderer: BatchSkinRenderer | null = null
let latestPreviewGeneration = 0
let previewGenerationQueue: Promise<void> = Promise.resolve()
function getSharedRenderer(): BatchSkinRenderer {
if (!sharedRenderer) {
sharedRenderer = new BatchSkinRenderer()
@@ -356,7 +365,27 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
return await generateHeadRender(skin)
}
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
export function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
const generation = ++latestPreviewGeneration
const skinsSnapshot = [...skins]
const capesSnapshot = [...capes]
const generationPromise = previewGenerationQueue.then(() =>
generateSkinPreviewsForGeneration(skinsSnapshot, capesSnapshot, generation),
)
previewGenerationQueue = generationPromise.catch(() => {})
return generationPromise
}
async function generateSkinPreviewsForGeneration(
skins: Skin[],
capes: Cape[],
generation: number,
): Promise<void> {
const isCurrentGeneration = () => generation === latestPreviewGeneration
try {
const skinKeys = skins.map(
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
@@ -368,6 +397,8 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
headStorage.batchRetrieve(headKeys),
])
if (!isCurrentGeneration()) return
for (let i = 0; i < skins.length; i++) {
const skinKey = skinKeys[i]
const headKey = headKeys[i]
@@ -388,6 +419,8 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
}
for (const skin of skins) {
if (!isCurrentGeneration()) return
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
if (skinBlobUrlMap.has(key)) {
@@ -419,6 +452,8 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
cape?.texture,
)
if (!isCurrentGeneration()) return
const renderResult: RenderResult = {
forwards: URL.createObjectURL(rawRenderResult.forwards),
backwards: URL.createObjectURL(rawRenderResult.backwards),
@@ -439,9 +474,12 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
}
} finally {
disposeSharedRenderer()
await cleanupUnusedPreviews(skins)
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
if (isCurrentGeneration()) {
await cleanupUnusedPreviews(skins)
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
}
}
}
+30 -10
View File
@@ -5,7 +5,6 @@ export interface Cape {
id: string
name: string
texture: string
is_default: boolean
is_equipped: boolean
}
@@ -15,6 +14,7 @@ export type SkinSource = 'default' | 'custom_external' | 'custom'
export interface Skin {
texture_key: string
name?: string
section?: string
variant: SkinModel
cape_id?: string
texture: string
@@ -121,17 +121,11 @@ export async function get_available_skins(): Promise<Skin[]> {
export async function add_and_equip_custom_skin(
textureBlob: Uint8Array,
variant: SkinModel,
capeOverride?: Cape,
): Promise<void> {
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
cape?: Cape,
): Promise<Skin> {
return await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
textureBlob,
variant,
capeOverride,
})
}
export async function set_default_cape(cape?: Cape): Promise<void> {
await invoke('plugin:minecraft-skins|set_default_cape', {
cape,
})
}
@@ -148,6 +142,22 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
})
}
export async function save_custom_skin(
skin: Skin,
textureBlob: Uint8Array,
variant: SkinModel,
cape: Cape | undefined,
replaceTexture: boolean,
): Promise<Skin> {
return await invoke('plugin:minecraft-skins|save_custom_skin', {
skin,
textureBlob,
variant,
cape,
replaceTexture,
})
}
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
const data = await normalize_skin_texture(skin.texture)
const base64 = arrayBufferToBase64(data)
@@ -162,6 +172,16 @@ export async function unequip_skin(): Promise<void> {
await invoke('plugin:minecraft-skins|unequip_skin')
}
export async function flush_pending_skin_change(): Promise<void> {
await invoke('plugin:minecraft-skins|flush_pending_skin_change')
}
export async function flush_pending_skin_change_for_profile(profileId: string): Promise<void> {
await invoke('plugin:minecraft-skins|flush_pending_skin_change_for_profile', {
profileId,
})
}
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
const data = await invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
return new Uint8Array(data)
+1
View File
@@ -10,6 +10,7 @@ export interface LoadingBarType {
version?: string
profile_path?: string
pack_name?: string
icon?: string | null
}
export interface LoadingBar {
@@ -0,0 +1,23 @@
import type { Labrinth } from '@modrinth/api-client'
const MIDAS_BITFLAG = 1 << 0
const PRIDE_26_MIDAS_DURATION_MS = 30 * 24 * 60 * 60 * 1000
type Pride26Campaign = Labrinth.Users.v3.Pride26CampaignDonation | null | undefined
export function hasMidasBadge(user?: { badges?: number } | null) {
return !!user?.badges && (user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG
}
export function hasPride26Badge(campaign: Pride26Campaign) {
return campaign?.has_badge === true
}
export function hasActivePride26Midas(campaign: Pride26Campaign, now = Date.now()) {
if (campaign?.has_midas !== true) {
return false
}
const donatedAt = new Date(campaign.last_donated_at).getTime()
return Number.isFinite(donatedAt) && donatedAt + PRIDE_26_MIDAS_DURATION_MS > now
}
@@ -5,6 +5,42 @@
"app.action-bar.downloads": {
"message": "التنزيلات"
},
"app.action-bar.hide-more-running-instances": {
"message": "إخفاء المزيد من الحالات قيد التشغيل"
},
"app.action-bar.make-primary-instance": {
"message": "إنشاء نسخة أساسية"
},
"app.action-bar.no-instances-running": {
"message": "لا توجد نسخ قيد التشغيل"
},
"app.action-bar.offline": {
"message": "غير متصل"
},
"app.action-bar.primary-instance": {
"message": "النسخة ألرئيسية"
},
"app.action-bar.show-more-running-instances": {
"message": "عرض المزيد من النسخ قيد التشغيل"
},
"app.action-bar.stop-instance": {
"message": "إيقاف النسخة"
},
"app.action-bar.view-active-downloads": {
"message": "عرض التحميلات الفعالة"
},
"app.action-bar.view-instance": {
"message": "عرض النسخة"
},
"app.action-bar.view-logs": {
"message": "عرض التسجيل"
},
"app.appearance-settings.default-landing-page.home": {
"message": "صفحة الرئيسية"
},
"app.appearance-settings.default-landing-page.library": {
"message": "مكتبة"
},
"app.auth-servers.unreachable.body": {
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
},
@@ -566,6 +602,15 @@
"instance.worlds.world_in_use": {
"message": "العالم قيد الاستخدام"
},
"minecraft-account.add-account": {
"message": "إضافة حساب"
},
"minecraft-account.select-account": {
"message": "اختيار الحساب"
},
"minecraft-account.sign-in": {
"message": "سجل دخولك لـ ماينكرافت"
},
"search.filter.locked.instance": {
"message": "مقدَّم من النسخة"
},
@@ -589,5 +634,8 @@
},
"search.filter.locked.server-loader.title": {
"message": "يتم توفير المحمّل من قبل الخادم"
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "لا تعرض هذا التحذير مجددا"
}
}
+248 -8
View File
@@ -1,10 +1,115 @@
{
"app.action-bar.downloading-java": {
"message": "Stahování Javy {version}"
},
"app.action-bar.downloads": {
"message": "Stažené Soubory"
},
"app.action-bar.hide-more-running-instances": {
"message": "Skrýt více spuštěných instancí"
},
"app.action-bar.make-primary-instance": {
"message": "Použít jako primární instanci"
},
"app.action-bar.no-instances-running": {
"message": "Žádné spuštěné instance"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primární instance"
},
"app.action-bar.show-more-running-instances": {
"message": "Zobrazit další spuštěné instance"
},
"app.action-bar.stop-instance": {
"message": "Ukončit instanci"
},
"app.action-bar.view-active-downloads": {
"message": "Zobrazit probíhající stahování"
},
"app.action-bar.view-instance": {
"message": "Zobrazit instanci"
},
"app.action-bar.view-logs": {
"message": "Zobrazit záznamy"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Umožňuje pokročilé vykreslování, jako jsou efekty rozostření, které mohou způsobit problémy s výkonem bez hardwarového vykreslení."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Pokročilé vykreslování"
},
"app.appearance-settings.color-theme.description": {
"message": "Vyberte si váš preferovaný motiv pro aplikaci Modrinth."
},
"app.appearance-settings.color-theme.title": {
"message": "Barevný motiv"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Změňte stránku, na které se spouštěč otevře."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Domov"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Knihovna"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Výchozí úvodní stránka"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Zakáže zobrazení jmenovky nad vaší postavou na stránce skinů."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Skrýt jmenovku"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "V sekci „Vrátit se do hry“ na úvodní stránce jsou uvedeny nejnovější světy."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Skočit zpět do světů"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizovat spouštěč při spuštění procesu Minecraftu."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimalizovat spouštěč"
},
"app.appearance-settings.native-decorations.description": {
"message": "Použít systémový rámeček okna (vyžaduje restart aplikace)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Nativní dekorace"
},
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Umožňí přepínat zobrazení postranního panelu."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Přepnout postranní panel"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Pokud se pokusíte nainstalovat soubor Modrinth Pack (.mrpack), který není hostován na Modrinth, zajistíme, abyste si před instalací uvědomili související rizika."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Upozorni mě před instalací neznámých modpacků"
},
"app.auth-servers.unreachable.body": {
"message": "Autořizační servery Minecraftu aktuálně nejsou k dispozici. Zkontrolujte si své připojení k internetu a zkuste to znovu později."
},
"app.auth-servers.unreachable.header": {
"message": "Připojení k autorizačním serverům se nezdařilo"
},
"app.browse.add-servers-to-instance": {
"message": "Přidat server do instance"
},
"app.browse.add-to-an-instance": {
"message": "Přidat do instalace"
},
"app.browse.add-to-instance": {
"message": "Přidat do instalace"
},
@@ -17,12 +122,33 @@
"app.browse.already-added": {
"message": "Už přidáno"
},
"app.browse.back-to-instance": {
"message": "Zpět k instanci"
},
"app.browse.discover-content": {
"message": "Prozkoumat obsah"
},
"app.browse.discover-servers": {
"message": "Prozkoumat servery"
},
"app.browse.hide-added-servers": {
"message": "Skrýt již přidané servery"
},
"app.browse.project-type.modpacks": {
"message": "Balíčky Modifikací"
},
"app.browse.server-instance-content-warning": {
"message": "Přidání obsahu může narušit kompatibilitu při připojení k serveru. Veškerý přidaný obsah se navíc ztratí při aktualizaci obsahu instance serveru."
},
"app.browse.server.installing": {
"message": "Instalování"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Probíhá instalace modpacku..."
},
"app.export-modal.description-placeholder": {
"message": "Přidej popis modpacku..."
},
@@ -32,12 +158,18 @@
"app.export-modal.header": {
"message": "Exportovat modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Zahrnout \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Jméno Modpacku"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Jméno modpacku"
},
"app.export-modal.select-files-label": {
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
},
"app.export-modal.version-number-label": {
"message": "Číslo verze"
},
@@ -92,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Procházet servery"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" bude **navždy smazáno** a nebude způsob, jak to obnovit."
},
"app.instance.worlds.delete-world-title": {
"message": "Opravdu si jsi jistý, že chceš navždy smazat tenhle svět?"
},
@@ -116,8 +245,8 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Žádné servery nebo světy nebyly přidány"
},
"app.instance.worlds.remove-server-description": {
"message": "{name} bude odstraněn z tvého seznamu, včetně hry, a nebude žádný způsob, jak ho obnovit."
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) bude odstraněn z vašeho seznamu, a to i ve hře, a nebude možné jej obnovit."
},
"app.instance.worlds.remove-server-title": {
"message": "Opravdu chceš odstranit {name}?"
@@ -128,6 +257,9 @@
"app.instance.worlds.this-server": {
"message": "tento server"
},
"app.modal.install-to-play.content-required": {
"message": "Požadovaný obsah"
},
"app.modal.install-to-play.header": {
"message": "Nainstaluj ke hraní"
},
@@ -137,12 +269,21 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural,one {#mód} other {#módy}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Povinný modpack"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "Tento server k hraní vyžaduje módy. Klikni na instalovat pro získání potřebných módů z Modrinth."
},
"app.modal.install-to-play.shared-instance": {
"message": "Sdílená instance"
},
"app.modal.install-to-play.shared-server-instance": {
"message": "Sdílená serverová instance"
},
"app.modal.install-to-play.view-contents": {
"message": "Zobrazit obsah"
},
"app.modal.update-to-play.header": {
"message": "Updatuj ke hraní"
},
@@ -152,6 +293,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Je nutná aktualizace ke hraní {name}. Prosím updatuj na nejnovější verzi k spuštění hry."
},
"app.project.install-button.already-installed": {
"message": "Tento projekt je již nainstalován"
},
"app.project.install-context.back-to-browse": {
"message": "Zpět na Prozkoumat"
},
"app.project.install-context.install-content-to-instance": {
"message": "Nainstalovat obsah do instnce"
},
"app.settings.developer-mode-enabled": {
"message": "Vývojářský režim povolen."
},
@@ -176,9 +326,33 @@
"app.settings.tabs.resource-management": {
"message": "Správa zdrojů"
},
"app.update-popup.body": {
"message": "Aplikace Modrinth v{version} je připravena k instalaci! Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-popup.body.download-complete": {
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-popup.body.linux": {
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
},
"app.update-popup.body.metered": {
"message": "Aplikace Modrinth v{version} je nyní k dispozici! Protože jste v měřené síti, nebyla stažena automaticky."
},
"app.update-popup.changelog": {
"message": "Seznam změn"
},
"app.update-popup.download": {
"message": "Stáhnout ({size})"
},
"app.update-popup.download-complete": {
"message": "Stahování bylo dokončeno"
},
"app.update-popup.reload": {
"message": "Načíst znovu"
},
"app.update-popup.title": {
"message": "Je k dispozici aktualizace"
},
"app.update.complete-toast.text": {
"message": "Kliknutím sem zobrazíte seznam změn."
},
@@ -194,6 +368,24 @@
"app.update.reload-to-update": {
"message": "Restartovat aplikaci pro nainstalování aktualizace"
},
"app.world.server-modal.placeholder-address": {
"message": "priklad.modrinth.gg"
},
"app.world.server-modal.select-an-option": {
"message": "Vyberte možnost"
},
"app.world.world-item.incompatible-version": {
"message": "Nekompatibilní verze {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Ještě nehráno"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Přidat přítele"
},
@@ -245,9 +437,6 @@
"friends.heading.pending": {
"message": "Čeká na vyřízení"
},
"friends.no-friends-match": {
"message": "Žádní přátelé neodpovídají '{query}''"
},
"friends.search-friends-placeholder": {
"message": "Hledat přátele..."
},
@@ -293,6 +482,12 @@
"instance.edit-world.title": {
"message": "Upravit svět"
},
"instance.files.adding-files": {
"message": "Přidávání souborů ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Uložit jako..."
},
"instance.server-modal.address": {
"message": "Adresa"
},
@@ -473,6 +668,9 @@
"instance.worlds.incompatible_server": {
"message": "Server je nekompatibilní"
},
"instance.worlds.linked_server": {
"message": "Spravováno projektem serveru"
},
"instance.worlds.no_contact": {
"message": "Server se nepodařilo spojit"
},
@@ -491,6 +689,24 @@
"instance.worlds.world_in_use": {
"message": "Svět je použit"
},
"minecraft-account.add-account": {
"message": "Přidat účet"
},
"minecraft-account.label": {
"message": "Účet Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Nejste přihlášeni"
},
"minecraft-account.remove-account": {
"message": "Odstranit účet"
},
"minecraft-account.select-account": {
"message": "Vyberte účet"
},
"minecraft-account.sign-in": {
"message": "Přihlásit se do Minecraftu"
},
"search.filter.locked.instance": {
"message": "Poskytováno instancí"
},
@@ -503,6 +719,9 @@
"search.filter.locked.instance.sync": {
"message": "Synchronizováno z instancí"
},
"search.filter.locked.server": {
"message": "Poskytováno serverem"
},
"search.filter.locked.server-environment.title": {
"message": "Jen klientské mody mohou být přidány na server"
},
@@ -511,5 +730,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader zprostředkovává server"
},
"unknown-pack-warning-modal.body": {
"message": "Soubor je zkontrolován pouze v případě, že je nahrán na Modrinth, a to bez ohledu na jeho formát (včetně formátu .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Toto varování již nezobrazovat"
},
"unknown-pack-warning-modal.header": {
"message": "Potvrdit instalaci"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Přesto nainstalovat"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Malware se často šíří prostřednictvím souborů s modpacky, které se sdílejí na platformách jako Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Tento soubor jsme na Modrinthu nenašli. Důrazně doporučujeme instalovat pouze soubory z důvěryhodných zdrojů."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Upozornění na neznámý soubor"
}
}
@@ -35,6 +35,9 @@
"app.action-bar.view-logs": {
"message": "Vis logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiverer avanceret gengivelse som sløringseffekter, der kan forårsage ydelsesproblemer uden hardwareaccelereret gengivelse."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Avanceret rendering"
},
@@ -77,6 +80,12 @@
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Aktivere muligheden for at slå sidepanelet til og fra."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Slå sidepanel til og fra"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Hvis du prøver at installere en Modrinth Pack fil (.mrpack) som ikke er hosted på Modrinth, så skal vi nok sikre os du forstår riskoen før du installere den."
},
@@ -89,6 +98,15 @@
"app.auth-servers.unreachable.header": {
"message": "Kan ikke nå autentificeringsservere"
},
"app.browse.add-servers-to-instance": {
"message": "Tilføjet server til instance"
},
"app.browse.add-to-an-instance": {
"message": "Tilføj til en instance"
},
"app.browse.add-to-instance": {
"message": "Tilføj til instance"
},
"app.browse.add-to-instance-name": {
"message": "Tilføjet til {instanceName}"
},
@@ -98,12 +116,33 @@
"app.browse.already-added": {
"message": "Allerede tilføjet"
},
"app.browse.back-to-instance": {
"message": "Tilbage til instance"
},
"app.browse.discover-content": {
"message": "Opdag indhold"
},
"app.browse.discover-servers": {
"message": "Opdag servere"
},
"app.browse.hide-added-servers": {
"message": "Gem allerede tilføjet til servere"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Ved at tilføje indhold kan der opstå kompatibilitetsproblemer, når du tilslutter dig serveren. Alt tilføjet indhold går desuden tabt, når du opdaterer indholdet på din server instance."
},
"app.browse.server.installing": {
"message": "Installere"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Installere modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Indtast modpack beskrivelse..."
},
@@ -113,12 +152,18 @@
"app.export-modal.header": {
"message": "Eksporter modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Inkludere \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpack Navn"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modpack navn"
},
"app.export-modal.select-files-label": {
"message": "Konfigurer hvilke filer inkludere i denne eksport"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
@@ -143,6 +188,12 @@
"app.instance.modpack-already-installed.create": {
"message": "Opret"
},
"app.instance.modpack-already-installed.header": {
"message": "Modpack allerede installeret"
},
"app.instance.modpack-already-installed.instance": {
"message": "Instance"
},
"app.instance.mods.content-type-project": {
"message": "projekt"
},
@@ -161,6 +212,42 @@
"app.instance.mods.successfully-uploaded": {
"message": "Uploadet"
},
"app.instance.worlds.add-server": {
"message": "Tilføj server"
},
"app.instance.worlds.browse-servers": {
"message": "Søg servere"
},
"app.instance.worlds.delete-world-title": {
"message": "Er du sikker på din vil slette denne verden permanent?"
},
"app.instance.worlds.filter-modded": {
"message": "Moddet"
},
"app.instance.worlds.filter-offline": {
"message": "Offline"
},
"app.instance.worlds.filter-online": {
"message": "Online"
},
"app.instance.worlds.filter-vanilla": {
"message": "Vanilla"
},
"app.instance.worlds.no-worlds-description": {
"message": "Tilføj en server, eller søg efter en for at komme i gang"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Ingen servere eller verdener tilføjet"
},
"app.instance.worlds.remove-server-title": {
"message": "Er du sikker på din vil fjerne {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Søg {count} verdener..."
},
"app.instance.worlds.this-server": {
"message": "denne server"
},
"app.modal.install-to-play.content-required": {
"message": "Indhold krævet"
},
@@ -197,6 +284,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
},
"app.project.install-button.already-installed": {
"message": "Dette projekt er allerede installeret"
},
"app.project.install-context.back-to-browse": {
"message": "Tilbage til udforsk"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer indhold til instance"
},
"app.settings.developer-mode-enabled": {
"message": "Udvikler-tilstand aktiveret."
},
@@ -221,6 +317,48 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcestyring"
},
"app.skins.add-button": {
"message": "Tilføj skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Træk og slip"
},
"app.skins.apply-button": {
"message": "Brug"
},
"app.skins.delete-button": {
"message": "Slet skin"
},
"app.skins.delete-modal.description": {
"message": "Dette vil permanent slette det valgte skin. Dette vil ikke kunne blive fortydes."
},
"app.skins.delete-modal.title": {
"message": "Er du sikker på, at du vil slette dette skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Fejl ved læsning af den afleverede fil."
},
"app.skins.dropped-file-error.title": {
"message": "Fejl under behandling af fil"
},
"app.skins.edit-button": {
"message": "Rediger skin"
},
"app.skins.modal.add-skin-button": {
"message": "Tilføj skin"
},
"app.skins.modal.add-title": {
"message": "Tilføjer et skin"
},
"app.skins.modal.arm-style-slim": {
"message": "Slank"
},
"app.skins.modal.arm-style-wide": {
"message": "Bred"
},
"app.skins.modal.cape-fallback-name": {
"message": "Kappe"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} er klar til at blive installeret! Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
},
@@ -563,6 +701,9 @@
"instance.worlds.world_in_use": {
"message": "Verden er i brug"
},
"minecraft-account.remove-account": {
"message": "Fjern konto"
},
"search.filter.locked.instance": {
"message": "Givet af instanceen"
},
+163 -10
View File
@@ -224,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Server durchsuchen"
},
"app.instance.worlds.delete-world-description": {
"message": "„{name}“ wird **endgültig gelöscht**, und es gibt keine Möglichkeit, sie wiederherzustellen."
},
"app.instance.worlds.delete-world-title": {
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
},
@@ -248,12 +245,6 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.remove-server-description": {
"message": "„{name}“ wird aus deiner Liste entfernt, auch im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "„{name}“ ({address}) wird aus deiner Liste entfernt, inklusive im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-title": {
"message": "Möchtest du {name} wirklich entfernen?"
},
@@ -303,7 +294,7 @@
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Durchstöbern"
"message": "Zurück zu Entdecken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
@@ -332,6 +323,147 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcenmanagement"
},
"app.skins.add-button": {
"message": "Skin hinzufügen"
},
"app.skins.add-button.drag-and-drop": {
"message": "Drag-and-Drop"
},
"app.skins.apply-button": {
"message": "Anwenden"
},
"app.skins.delete-button": {
"message": "Skin löschen"
},
"app.skins.delete-modal.description": {
"message": "Dies wird den ausgewählten Skin unwiderruflich löschen. Diese Aktion kann nicht rückgängig gemacht werden."
},
"app.skins.delete-modal.title": {
"message": "Bist du dir sicher, dass du diesen Skin löschen willst?"
},
"app.skins.dropped-file-error.text": {
"message": "Die abgelegte Datei konnte nicht gelesen werden."
},
"app.skins.dropped-file-error.title": {
"message": "Fehler bei der Verarbeitung der Datei"
},
"app.skins.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.modal.add-skin-button": {
"message": "Skin hinzufügen"
},
"app.skins.modal.add-title": {
"message": "Skin wird hinzugefügt"
},
"app.skins.modal.arm-style-section": {
"message": "Arm Stil"
},
"app.skins.modal.arm-style-slim": {
"message": "Schlank"
},
"app.skins.modal.arm-style-wide": {
"message": "Breit"
},
"app.skins.modal.cape-fallback-name": {
"message": "Umhang"
},
"app.skins.modal.cape-section": {
"message": "Umhang"
},
"app.skins.modal.edit-title": {
"message": "Skin bearbeiten"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Nimm zuerst eine Änderung am Skin vor!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Kein Umhang"
},
"app.skins.modal.none-cape-option": {
"message": "Keinen"
},
"app.skins.modal.replace-texture-button": {
"message": "Textur ersetzen"
},
"app.skins.modal.save-skin-button": {
"message": "Skin speichern"
},
"app.skins.modal.saving-tooltip": {
"message": "Wird gespeichert..."
},
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Lade zuerst ein Skin hoch!"
},
"app.skins.preview.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.previewing-badge": {
"message": "Vorschau"
},
"app.skins.rate-limit.text": {
"message": "Du wechselst dein Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
},
"app.skins.rate-limit.title": {
"message": "Langsamer!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Standard-Skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Du hast diese Skins erhalten, da du während des Pride-Monats für eine Modrinth-Pride-Spendenaktion gespendet hast."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Gespeicherte Skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Anmelden"
},
"app.skins.sign-in.description": {
"message": "Bitte melde dich bei deinem Minecraft-Konto an, um die Funktionen zur Skin-Verwaltung der Modrinth-App nutzen zu können."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Begeisterter Modrinth-Bot"
},
"app.skins.sign-in.title": {
"message": "Bitte melde dich an"
},
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
},
@@ -611,6 +743,24 @@
"instance.settings.tabs.java": {
"message": "Java und Arbeitsspeicher"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Benutzerdefinierte Umgebungsvariablen"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Benutzerdefinierte Java-Argumente"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Benutzerdefinierte Java-Installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Benutzerdefinierte Speicherzuweisung"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Umgebungsvariablen eingeben..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java-Argumente eingeben..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Umgebungsvariablen"
},
@@ -626,6 +776,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Zugewiesener Arbeitsspeicher"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/pfad/zu/java"
},
"instance.settings.tabs.window": {
"message": "Spielfenster"
},
+170 -11
View File
@@ -48,7 +48,7 @@
"message": "Farbschema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
"message": "Ändere die Kategorie, die beim Öffnen des Launchers angezeigt wird."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
@@ -137,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Das Hinzufügen von Inhalten kann die Kompatibilität beim Beitreten zum Server beeinträchtigen. Außerdem gehen alle hinzugefügten Inhalte verloren, wenn du die Inhalte der Serverinstanz aktualisierst."
},
"app.browse.server.installing": {
"message": "Wird installiert"
},
@@ -221,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Server durchsuchen"
},
"app.instance.worlds.delete-world-description": {
"message": "„{name}“ wird **endgültig gelöscht**, und es gibt keine Möglichkeit, sie wiederherzustellen."
},
"app.instance.worlds.delete-world-title": {
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
},
@@ -245,12 +245,6 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Keine Server oder Welten hinzugefügt"
},
"app.instance.worlds.remove-server-description": {
"message": "„{name}“ wird aus deiner Liste entfernt, auch im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "„{name}“ ({address}) wird aus deiner Liste entfernt, inklusive im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
},
"app.instance.worlds.remove-server-title": {
"message": "Möchtest du {name} wirklich entfernen?"
},
@@ -299,6 +293,9 @@
"app.project.install-button.already-installed": {
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Entdecken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
@@ -326,6 +323,147 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcenmanagement"
},
"app.skins.add-button": {
"message": "Skin hinzufügen"
},
"app.skins.add-button.drag-and-drop": {
"message": "Ziehen und Ablegen"
},
"app.skins.apply-button": {
"message": "Anwenden"
},
"app.skins.delete-button": {
"message": "Skin löschen"
},
"app.skins.delete-modal.description": {
"message": "Dadurch wird der ausgewählte Skin permanent gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden."
},
"app.skins.delete-modal.title": {
"message": "Möchtest du diesen Skin wirklich löschen?"
},
"app.skins.dropped-file-error.text": {
"message": "Die abgelegte Datei konnte nicht gelesen werden."
},
"app.skins.dropped-file-error.title": {
"message": "Fehler bei der Verarbeitung der Datei"
},
"app.skins.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.modal.add-skin-button": {
"message": "Skin hinzufügen"
},
"app.skins.modal.add-title": {
"message": "Einen Skin hinzufügen"
},
"app.skins.modal.arm-style-section": {
"message": "Arm-Stil"
},
"app.skins.modal.arm-style-slim": {
"message": "Schlank"
},
"app.skins.modal.arm-style-wide": {
"message": "Breit"
},
"app.skins.modal.cape-fallback-name": {
"message": "Umhang"
},
"app.skins.modal.cape-section": {
"message": "Umhang"
},
"app.skins.modal.edit-title": {
"message": "Skin bearbeiten"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Nimm zuerst eine Änderung am Skin vor!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Kein Umhang"
},
"app.skins.modal.none-cape-option": {
"message": "Keinen"
},
"app.skins.modal.replace-texture-button": {
"message": "Textur ersetzen"
},
"app.skins.modal.save-skin-button": {
"message": "Skin speichern"
},
"app.skins.modal.saving-tooltip": {
"message": "Wird gespeichert..."
},
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Lade zuerst ein Skin hoch!"
},
"app.skins.preview.edit-button": {
"message": "Skin bearbeiten"
},
"app.skins.previewing-badge": {
"message": "Vorschau"
},
"app.skins.rate-limit.text": {
"message": "Du wechselst dein Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
},
"app.skins.rate-limit.title": {
"message": "Langsamer!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Standard-Skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Du hast diese Skins erhalten, da du während des Pride-Monats für eine Modrinth-Pride-Spendenaktion gespendet hast."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Gespeicherte Skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Anmelden"
},
"app.skins.sign-in.description": {
"message": "Bitte melde dich bei deinem Minecraft-Konto an, um die Funktionen zur Skin-Verwaltung der Modrinth-App nutzen zu können."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Begeisterter Modrinth-Bot"
},
"app.skins.sign-in.title": {
"message": "Bitte melde dich an"
},
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
},
@@ -519,7 +657,7 @@
"message": "Wird gelöscht..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Kopieren"
"message": "Duplizieren"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Kann während der Installation nicht dupliziert werden."
@@ -605,6 +743,24 @@
"instance.settings.tabs.java": {
"message": "Java und Arbeitsspeicher"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Benutzerdefinierte Umgebungsvariablen"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Benutzerdefinierte Java-Argumente"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Benutzerdefinierte Java-Installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Benutzerdefinierte Speicherzuweisung"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Umgebungsvariablen eingeben..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java-Argumente eingeben..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Umgebungsvariablen"
},
@@ -620,6 +776,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Zugewiesener Arbeitsspeicher"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/pfad/zu/java"
},
"instance.settings.tabs.window": {
"message": "Fenster"
},
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Select an option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Displays how much time you've spent playing an instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Show play time"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Enables the ability to toggle the sidebar."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Installing"
},
"app.content-install.no-compatible-versions": {
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -332,6 +341,147 @@
"app.settings.tabs.resource-management": {
"message": "Resource management"
},
"app.skins.add-button": {
"message": "Add skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Drag and drop"
},
"app.skins.apply-button": {
"message": "Apply"
},
"app.skins.delete-button": {
"message": "Delete skin"
},
"app.skins.delete-modal.description": {
"message": "This will permanently delete the selected skin. This action cannot be undone."
},
"app.skins.delete-modal.title": {
"message": "Are you sure you want to delete this skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Failed to read the dropped file."
},
"app.skins.dropped-file-error.title": {
"message": "Error processing file"
},
"app.skins.edit-button": {
"message": "Edit skin"
},
"app.skins.modal.add-skin-button": {
"message": "Add skin"
},
"app.skins.modal.add-title": {
"message": "Adding a skin"
},
"app.skins.modal.arm-style-section": {
"message": "Arm style"
},
"app.skins.modal.arm-style-slim": {
"message": "Slim"
},
"app.skins.modal.arm-style-wide": {
"message": "Wide"
},
"app.skins.modal.cape-fallback-name": {
"message": "Cape"
},
"app.skins.modal.cape-section": {
"message": "Cape"
},
"app.skins.modal.edit-title": {
"message": "Editing skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Make an edit to the skin first!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "No cape"
},
"app.skins.modal.none-cape-option": {
"message": "None"
},
"app.skins.modal.replace-texture-button": {
"message": "Replace texture"
},
"app.skins.modal.save-skin-button": {
"message": "Save skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Saving..."
},
"app.skins.modal.texture-section": {
"message": "Texture"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Upload a skin first!"
},
"app.skins.preview.edit-button": {
"message": "Edit skin"
},
"app.skins.previewing-badge": {
"message": "Previewing"
},
"app.skins.rate-limit.text": {
"message": "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again."
},
"app.skins.rate-limit.title": {
"message": "Slow down!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Default skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "You received these skins for donating to a Modrinth Pride fundraiser during Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Saved skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Sign In"
},
"app.skins.sign-in.description": {
"message": "Please sign into your Minecraft account to use the skin management features of the Modrinth app."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Excited Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Please sign in"
},
"app.skins.title": {
"message": "Skin selector"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App."
},
@@ -611,6 +761,24 @@
"instance.settings.tabs.java": {
"message": "Java and memory"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Custom environment variables"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Custom Java arguments"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Custom Java installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Custom memory allocation"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Enter environmental variables..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Enter Java arguments..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Environment variables"
},
@@ -626,6 +794,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memory allocated"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Window"
},
+174 -9
View File
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede acceder a los servidores de autenticación"
},
"app.browse.add-servers-to-instance": {
"message": "Añadiendo servidor a instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a instancia"
},
@@ -116,6 +122,9 @@
"app.browse.already-added": {
"message": "Ya se ha añadido"
},
"app.browse.back-to-instance": {
"message": "Volver a instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
@@ -128,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede romper la compatibilidad cuando te unes al servidor. Cualquier contenido añadido tambien se perderá cuando actualizes el contenido en la instancia del servidor."
},
"app.browse.server.installing": {
"message": "Instalando"
},
@@ -212,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Explorar servidores"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" se **eliminará permanentemente** y no habrá forma de recuperarlo."
},
"app.instance.worlds.delete-world-title": {
"message": "¿Estás seguro de que quieres eliminar este mundo de forma permanente?"
},
@@ -236,12 +245,6 @@
"app.instance.worlds.no-worlds-heading": {
"message": "No hay servidores ni mundos añadidos"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
},
"app.instance.worlds.remove-server-title": {
"message": "¿Estás seguro de que quieres eliminar {name}?"
},
@@ -287,6 +290,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Se requiere una actualización para jugar {name}. Por favor, actualiza a la versión más reciente para iniciar el juego."
},
"app.project.install-button.already-installed": {
"message": "Este proyecto ya está instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Volver a descubrir contenido"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
},
"app.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
@@ -311,6 +323,138 @@
"app.settings.tabs.resource-management": {
"message": "Gestión de recursos"
},
"app.skins.add-button": {
"message": "Añadir skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Arrastrar y soltar"
},
"app.skins.apply-button": {
"message": "Aplicar"
},
"app.skins.delete-button": {
"message": "Eliminar skin"
},
"app.skins.delete-modal.description": {
"message": "Esto eliminará permanentemente el skin seleccionado. Esta acción no se puede deshacer."
},
"app.skins.delete-modal.title": {
"message": "¿Estás seguro de que quieres eliminar este skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Error al leer el archivo arrastrado."
},
"app.skins.dropped-file-error.title": {
"message": "Error al procesar el archivo"
},
"app.skins.edit-button": {
"message": "Editar skin"
},
"app.skins.modal.add-skin-button": {
"message": "Añadir skin"
},
"app.skins.modal.add-title": {
"message": "Añadiendo un skin"
},
"app.skins.modal.arm-style-section": {
"message": "Estilo de brazo"
},
"app.skins.modal.arm-style-slim": {
"message": "Delgado"
},
"app.skins.modal.arm-style-wide": {
"message": "Ancho"
},
"app.skins.modal.cape-fallback-name": {
"message": "Capa"
},
"app.skins.modal.cape-section": {
"message": "Capa"
},
"app.skins.modal.edit-title": {
"message": "Editando skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "¡Primero edita el skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Sin capa"
},
"app.skins.modal.none-cape-option": {
"message": "Ninguna"
},
"app.skins.modal.replace-texture-button": {
"message": "Remplazar textura"
},
"app.skins.modal.save-skin-button": {
"message": "Guardar skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Guardando..."
},
"app.skins.modal.texture-section": {
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "¡Primero carga un skin!"
},
"app.skins.preview.edit-button": {
"message": "Editar skin"
},
"app.skins.previewing-badge": {
"message": "Previsualizando"
},
"app.skins.rate-limit.text": {
"message": "Estás cambiando tu skin muy frecuentemente. Los servidores de Mojang han bloqueado temporalmente las solicitdues futuras. Por favor, espera un momento antes de volver a intentarlo."
},
"app.skins.rate-limit.title": {
"message": "¡Más despacio!"
},
"app.skins.section.builders-and-biomes": {
"message": "Constructores y Biomas"
},
"app.skins.section.chase-the-skies": {
"message": "Persigue los Cielos"
},
"app.skins.section.default-skins": {
"message": "Skins predeterminadas"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Monturas del Caos"
},
"app.skins.section.saved-skins": {
"message": "Skins guardados"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "La Era del Cobre"
},
"app.skins.section.the-garden-awakens": {
"message": "El Jardín Despierta"
},
"app.skins.section.tiny-takeover": {
"message": "Bebés al Poder"
},
"app.skins.sign-in.button": {
"message": "Iniciar sesión"
},
"app.skins.sign-in.description": {
"message": "Por favor, inicia sesión en tu cuenta de Minecraft para poder utilizar las funciones de gestión de skins de la Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emocionado"
},
"app.skins.sign-in.title": {
"message": "Por favor, inicia sesión"
},
"app.skins.title": {
"message": "Selector de skin"
},
"app.update-popup.body": {
"message": "¡Modrinth App v{version} está lista para instalarse! Actualiza ahora o automáticamente al cerrar la Modrinth App."
},
@@ -590,6 +734,24 @@
"instance.settings.tabs.java": {
"message": "Java y memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variables de entorno personalizadas"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumentos personalizados de Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Instalación personalizada de Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Asignación de memoria personalizada"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Introduce las variables de entorno..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Introduce los argumentos de Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variables de entorno"
},
@@ -605,6 +767,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memoria asignada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/ruta/de/java"
},
"instance.settings.tabs.window": {
"message": "Ventana"
},
+83 -8
View File
@@ -21,7 +21,7 @@
"message": "Instancia principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mas instancias en ejecución"
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Finalizar instancia"
@@ -48,7 +48,7 @@
"message": "Tema de color"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que el launcher se abre en."
"message": "Cambia la página en la que el lanzador inicia."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
@@ -72,13 +72,13 @@
"message": "Vuelve a jugar tus mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiza el launcher cuando un proceso de Minecraft empieza."
"message": "Minimiza el lanzador cuando un proceso de Minecraft empieza."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiza el launcher"
"message": "Minimiza el lanzador"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa los sistemas de frame de Windows (se requiere resetear la aplicación)."
"message": "Usa los sistemas de marco de Windows (se requiere resetear la aplicación)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
@@ -93,10 +93,10 @@
"message": "Alternar barra lateral"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, te advertiremos de los riesgos antes de instalarlo."
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, debes comprender los riesgos que esto conlleva antes de instalarlo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Adviérteme antes de instalarme modpacks desconocidos"
"message": "Advertir antes de instalarme paquetes de mods desconocidos"
},
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede conectar con los servidores de autenticación"
},
"app.browse.add-servers-to-instance": {
"message": "Añadir servidor a instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a la instancia"
},
@@ -116,6 +122,9 @@
"app.browse.already-added": {
"message": "Ya está añadido"
},
"app.browse.back-to-instance": {
"message": "Volver a la instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
@@ -126,7 +135,10 @@
"message": "Ocultar servidores ya añadidos"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"message": "Paquetes de Mods"
},
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede comprometer la compatibilidad a la hora de unirse a un servidor. Cualquier contenido añadido se perderá cuando actualices la instancia del servidor."
},
"app.browse.server.installing": {
"message": "Instalando"
@@ -146,12 +158,18 @@
"app.export-modal.header": {
"message": "Exportar modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "¿Incluir \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nombre del modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Configura que archivos se incluirán en esta instancia"
},
"app.export-modal.version-number-label": {
"message": "Número de versión"
},
@@ -281,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Una actualización es requerida para jugar {name}. Por favor actualízala a la versión más reciente para ejecutar el juego."
},
"app.project.install-button.already-installed": {
"message": "El proyecto ya está instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Volver a descubrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar contenido"
},
"app.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
@@ -305,6 +332,15 @@
"app.settings.tabs.resource-management": {
"message": "Gestión de recursos"
},
"app.skins.add-button": {
"message": "Añadir skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Arrastrar y soltar"
},
"app.skins.apply-button": {
"message": "Aplicar"
},
"app.update-popup.body": {
"message": "¡La versión v{version} de Modrinth está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación."
},
@@ -584,6 +620,24 @@
"instance.settings.tabs.java": {
"message": "Java y memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variables de entorno personalizadas"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumentos personalizados de Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Instalación personalizada de Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Asignación de memoria personalizada"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Introduce las variables de entorno..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Introduce los argumentos de Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variables de entorno"
},
@@ -599,6 +653,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memoria asignada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/ruta/a/java"
},
"instance.settings.tabs.window": {
"message": "Ventana"
},
@@ -671,6 +728,24 @@
"instance.worlds.world_in_use": {
"message": "El mundo está en uso"
},
"minecraft-account.add-account": {
"message": "Añadir cuenta"
},
"minecraft-account.label": {
"message": "Cuenta de Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Registrarse"
},
"minecraft-account.remove-account": {
"message": "Eliminar cuenta"
},
"minecraft-account.select-account": {
"message": "Seleccionar cuenta"
},
"minecraft-account.sign-in": {
"message": "Registrarse en Minecraft"
},
"search.filter.locked.instance": {
"message": "Proporcionado por la instancia"
},
@@ -1,10 +1,115 @@
{
"app.action-bar.downloading-java": {
"message": "Ladataan Java-versiota {version}"
},
"app.action-bar.downloads": {
"message": "Lataukset"
},
"app.action-bar.hide-more-running-instances": {
"message": "Piilota enemmän käynnissä olevia instansseja"
},
"app.action-bar.make-primary-instance": {
"message": "Aseta ensisijaiseksi instanssiksi"
},
"app.action-bar.no-instances-running": {
"message": "Ei instansseja käynnissä"
},
"app.action-bar.offline": {
"message": "Offline-tilassa"
},
"app.action-bar.primary-instance": {
"message": "Ensisijainen instanssi"
},
"app.action-bar.show-more-running-instances": {
"message": "Näytä enemmän käynnissä olevia instansseja"
},
"app.action-bar.stop-instance": {
"message": "Pysäytä instanssi"
},
"app.action-bar.view-active-downloads": {
"message": "Näytä aktiiviset lataukset"
},
"app.action-bar.view-instance": {
"message": "Katso intanssia"
},
"app.action-bar.view-logs": {
"message": "Katso lokeja"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Mahdollistaa edistyneen renderöinnin, kuten sumennus efektit, jotka voivat aiheuttaa suorituskykyongelmia ilman laitteistokiihdytettyä renderöintiä."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Edistynyt renderointi"
},
"app.appearance-settings.color-theme.description": {
"message": "Valitse haluamasi väriteema Modrinth Appille."
},
"app.appearance-settings.color-theme.title": {
"message": "Väriteema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Muuta sivua mille laukaisin avautuu."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Koti"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Kirjasto"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Oletus laskeutumissivu"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Kytkee pois päältä nimikyltin pelaajasi päällä skinit sivulla."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Piilota nimikyltti"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Sisällyttää viimeaikaiset maailmat \"Hyppää takaisin sisään\" osiossa Koti sivulla."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Hyppää takaisin sisään maailmoihin"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Pienennä laukaisin kun Minecraft prosessi käynnistyy."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Piilota käynnistysohjelma"
},
"app.appearance-settings.native-decorations.description": {
"message": "Käytä järjestelmän ikkuna kehystä (Vaatii sovelluksen uudelleen käynnistämisen)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Natiivit koristukset"
},
"app.appearance-settings.select-option": {
"message": "Valitse vaihtoehto"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Laittaa päälle mahdollisuuden kytkeä sivupalkkia."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Kytke sivupalkki"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Jos yrität asentaa Modrinth-pakettitiedoston (.mrpack), jota ei ole isännöity Modrinthissä, varmistamme, että ymmärrät riskit ennen asennusta."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Varoita minua ennen tuntemattomien modipakettien asentamista"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraftin todennuspalvelimet eivät ehkä ole tällä hetkellä tavoitettavissa. Tarkista internetyhteytesi ja yritä myöhemmin uudelleen."
},
"app.auth-servers.unreachable.header": {
"message": "Todennuspalvelimiin ei saada yhteyttä"
},
"app.browse.add-servers-to-instance": {
"message": "Lisää palvelin instanssiin"
},
"app.browse.add-to-an-instance": {
"message": "Lisää instanssiin"
},
"app.browse.add-to-instance": {
"message": "Lisää instanssiin"
},
@@ -17,12 +122,33 @@
"app.browse.already-added": {
"message": "On jo asennettu"
},
"app.browse.back-to-instance": {
"message": "Takaisin instanssiin"
},
"app.browse.discover-content": {
"message": "Löydä sisältöä"
},
"app.browse.discover-servers": {
"message": "Löydä palvelimia"
},
"app.browse.hide-added-servers": {
"message": "Piilota lisätyt palvelimet"
},
"app.browse.project-type.modpacks": {
"message": "Modipaketit"
},
"app.browse.server-instance-content-warning": {
"message": "Sisällön lisääminen voi rikkoa yhteensopivuuden palvelimelle liittyessä. Kaikki lisätty sisältö katoaa myös kun päivität palvelin instanssin sisällön."
},
"app.browse.server.installing": {
"message": "Asennetaan"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Asennetaan modipakettia..."
},
"app.export-modal.description-placeholder": {
"message": "Lisää modipaketin kuvaus..."
},
@@ -32,12 +158,18 @@
"app.export-modal.header": {
"message": "Vie modipaketti"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sisällytä \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modipaketin nimi"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modipaketin nimi"
},
"app.export-modal.select-files-label": {
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
},
"app.export-modal.version-number-label": {
"message": "Versio numero"
},
@@ -80,6 +212,9 @@
"app.instance.mods.share-text": {
"message": "Tsekkaa projektit joita käytän minun modipaketissani!"
},
"app.instance.mods.share-title": {
"message": "Jaetaan modipaketti sisältöä"
},
"app.instance.mods.successfully-uploaded": {
"message": "Onnistuneesti ladattu"
},
@@ -164,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Pävitys vaaditaan pelataksesi {name}. Päivitä viimeisimpään versioon pelataksesi."
},
"app.project.install-button.already-installed": {
"message": "Tämä projekti on jo asennettu"
},
"app.project.install-context.back-to-browse": {
"message": "Takaisin sisällön löytöön"
},
"app.project.install-context.install-content-to-instance": {
"message": "Asenna sisältöä instanssiin"
},
"app.settings.developer-mode-enabled": {
"message": "Kehittäjätila käytössä."
},
@@ -467,6 +611,24 @@
"instance.settings.tabs.java": {
"message": "Java ja muisti"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Mukautetut ympäristö muuttujat"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Mukautetut Java argumentit"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Mukautettu Java asennus"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Mukautettu muistin allokointi"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Syötä ympäristö muuttujat..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Syötä Java argumentit..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Ympäristö muuttujat"
},
@@ -482,6 +644,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Muisti allokoitu"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/javaan/johtava/polku/"
},
"instance.settings.tabs.window": {
"message": "Ikkuna"
},
@@ -554,6 +719,24 @@
"instance.worlds.world_in_use": {
"message": "Maailma on käytössä"
},
"minecraft-account.add-account": {
"message": "Lisää tili"
},
"minecraft-account.label": {
"message": "Minecraft tili"
},
"minecraft-account.not-signed-in": {
"message": "Ei kirjautuneena sisään"
},
"minecraft-account.remove-account": {
"message": "Poista tili"
},
"minecraft-account.select-account": {
"message": "Valitse tili"
},
"minecraft-account.sign-in": {
"message": "Kirjaudu sisään Minecraftiin"
},
"search.filter.locked.instance": {
"message": "Annettu instanssin toimesta"
},
@@ -577,5 +760,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Modialusta on palvelimen tarjoama"
},
"unknown-pack-warning-modal.body": {
"message": "Tiedosto tarkistetaan vain jos se on ladattu Modrinthiin, riippumatta tiedoston muodosta (mukaanlukien .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Älä näytä tätä varoitusta uudestaan"
},
"unknown-pack-warning-modal.header": {
"message": "Vahvista asennus"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Asenna jokatapauksessa"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Haittaohjelmia levitetään usein modipaketti tiedostojen kautta jakamalla niitä alustoilla kuten Discordissa."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Emme löytäneet tätä tiedostoa Modrinthista. Suosittelemme vahvasti että asennat tiedostoja vain lähteistä joihin luotat."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Tuntematon tiedosto varoitus"
}
}
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "Hindi maabot ang mga authentication server"
},
"app.browse.add-servers-to-instance": {
"message": "Idinaragdag ang server sa instansiya"
},
"app.browse.add-to-an-instance": {
"message": "Idagdag sa instansiya"
},
"app.browse.add-to-instance": {
"message": "Idagdag sa instansiya"
},
@@ -116,6 +122,9 @@
"app.browse.already-added": {
"message": "Nadagdag na"
},
"app.browse.back-to-instance": {
"message": "Bumalik sa instansiya"
},
"app.browse.discover-content": {
"message": "Tumuklas ng kontento"
},
@@ -128,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Maaaring makasisira sa pagkakatugma sa pagsasali ng server ang pagdaragdag ng kontento. Ang anumang kontento ay mawawala sa pag-update ng kontento ng instansiyang pang-server."
},
"app.browse.server.installing": {
"message": "Ini-install"
},
@@ -287,6 +299,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "Kailangang mag-update upang malaro ang {name}. Mangyaring mag-update sa pinakabagong bersiyon upang ma-launch ang laro."
},
"app.project.install-button.already-installed": {
"message": "Naka-install na ang proyektong ito"
},
"app.project.install-context.back-to-browse": {
"message": "Bumalik sa pagtutuklas"
},
"app.settings.developer-mode-enabled": {
"message": "Binuksan ang moda ng nagdi-develop."
},
@@ -311,6 +329,36 @@
"app.settings.tabs.resource-management": {
"message": "Pamamahala ng paglalaan"
},
"app.skins.apply-button": {
"message": "Ilapat"
},
"app.skins.modal.arm-style-slim": {
"message": "Payat"
},
"app.skins.modal.arm-style-wide": {
"message": "Malapad"
},
"app.skins.modal.cape-fallback-name": {
"message": "Kapa"
},
"app.skins.modal.cape-section": {
"message": "Kapa"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Walang kapa"
},
"app.skins.modal.none-cape-option": {
"message": "Wala"
},
"app.skins.modal.texture-section": {
"message": "Texture"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.update-popup.body": {
"message": "Ang Modrinth App v{version} ay handa nang ma-install. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
},
@@ -729,7 +777,7 @@
"message": "Kumpirmahin ang installation"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "I-install parin"
"message": "I-install pa rin"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Ang malware ay madalas naidadala sa mga modpack files sa pamamagitan ng pag-bigay ng mga ito sa mga platforms kagaya ng Discord."
+164 -14
View File
@@ -1,6 +1,6 @@
{
"app.action-bar.downloading-java": {
"message": "Java version {version} en cours de téléchargement"
"message": "Téléchargement de Java {version} en cours"
},
"app.action-bar.downloads": {
"message": "Téléchargements"
@@ -224,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Parcourir des serveurs"
},
"app.instance.worlds.delete-world-description": {
"message": "« {name} » sera supprimé **pour toujours**, et il sera impossible de le récupérer."
},
"app.instance.worlds.delete-world-title": {
"message": "Êtes-vous sûr·e de vouloir supprimer ce monde pour toujours ?"
},
@@ -248,12 +245,6 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Aucun serveur ou monde ajoutés"
},
"app.instance.worlds.remove-server-description": {
"message": "« {name} » sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "« {name} » ({address}) sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
},
"app.instance.worlds.remove-server-title": {
"message": "Êtes-vous sûr·e de vouloir retirer {name} ?"
},
@@ -303,7 +294,7 @@
"message": "Ce projet a déjà été installé"
},
"app.project.install-context.back-to-browse": {
"message": "Retour à la navigation"
"message": "Retour vers la page découvrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
@@ -332,6 +323,147 @@
"app.settings.tabs.resource-management": {
"message": "Gestion des ressources"
},
"app.skins.add-button": {
"message": "Ajouter une skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Glisser et déposer"
},
"app.skins.apply-button": {
"message": "Appliquer"
},
"app.skins.delete-button": {
"message": "Supprimer la skin"
},
"app.skins.delete-modal.description": {
"message": "Cela va supprimer de manière permanente cette skin. Cette action est irréversible."
},
"app.skins.delete-modal.title": {
"message": "Êtes-vous sûr de vouloir supprimer cette skin ?"
},
"app.skins.dropped-file-error.text": {
"message": "Impossible de lire le fichier déposé."
},
"app.skins.dropped-file-error.title": {
"message": "Erreur lors du traitement du fichier"
},
"app.skins.edit-button": {
"message": "Modifier la skin"
},
"app.skins.modal.add-skin-button": {
"message": "Ajouter une skin"
},
"app.skins.modal.add-title": {
"message": "Ajouter une skin"
},
"app.skins.modal.arm-style-section": {
"message": "Bras"
},
"app.skins.modal.arm-style-slim": {
"message": "Mince"
},
"app.skins.modal.arm-style-wide": {
"message": "Large"
},
"app.skins.modal.cape-fallback-name": {
"message": "Cape"
},
"app.skins.modal.cape-section": {
"message": "Cape"
},
"app.skins.modal.edit-title": {
"message": "Modification de la skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Faites d'abord une modification à la skin !"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Aucune cape"
},
"app.skins.modal.none-cape-option": {
"message": "Aucune"
},
"app.skins.modal.replace-texture-button": {
"message": "Remplacer la texture"
},
"app.skins.modal.save-skin-button": {
"message": "Sauvegarder la skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Sauvegarde..."
},
"app.skins.modal.texture-section": {
"message": "Texture"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Importez d'abord une skin !"
},
"app.skins.preview.edit-button": {
"message": "Modifier la skin"
},
"app.skins.previewing-badge": {
"message": "Aperçu"
},
"app.skins.rate-limit.text": {
"message": "Vous changez de skin trop vite. Les serveurs de Mojang ont temporairement bloqué certaines requêtes. Veuillez patienter un moment puis réessayez."
},
"app.skins.rate-limit.title": {
"message": "Du calme !"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chassez les cieux"
},
"app.skins.section.default-skins": {
"message": "Skins par défaut"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Vous avez reçu ces skins pour avoir fait un don à une collecte de fonds Modrinth Pride pendant le mois de la fierté."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Montures en pagaille"
},
"app.skins.section.saved-skins": {
"message": "Skins sauvegardées"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "L'éveil du jardin"
},
"app.skins.section.tiny-takeover": {
"message": "Horde miniature"
},
"app.skins.sign-in.button": {
"message": "Se connecter"
},
"app.skins.sign-in.description": {
"message": "Veuillez vous connecter à votre compte Minecraft pour utiliser les fonctionnalités de gestion de skins de Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot Modrinth enthousiaste"
},
"app.skins.sign-in.title": {
"message": "Veuillez vous connecter"
},
"app.skins.title": {
"message": "Sélecteur de skin"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} est prête à être installée ! Relancez l'application pour faire la mise à jour maintenant, ou automatiquement à la fermeture de Modrinth App."
},
@@ -389,9 +521,6 @@
"app.world.world-item.offline": {
"message": "Hors ligne"
},
"app.world.world-item.players-online": {
"message": "{count, plural, =0 {Personne en ligne} one {# est en ligne} other {# sont en ligne}}"
},
"friends.action.add-friend": {
"message": "Ajouter un ami"
},
@@ -611,6 +740,24 @@
"instance.settings.tabs.java": {
"message": "Java et mémoire"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variables d'environnement personnalisé"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Arguments Java personnalisée"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Installation Java personnalisée"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Allocation de mémoire personnalisée"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Entrer une variable d'environnement..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Entrer un argument Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variables d'environnement"
},
@@ -626,6 +773,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Mémoire allouée"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/chemin/vers/java"
},
"instance.settings.tabs.window": {
"message": "Fenêtre"
},
+12 -3
View File
@@ -1,4 +1,16 @@
{
"app.action-bar.downloads": {
"message": "הורדות"
},
"app.action-bar.offline": {
"message": "לא מקוון"
},
"app.action-bar.stop-instance": {
"message": ""
},
"app.appearance-settings.advanced-rendering.description": {
"message": "מאפשר עיבוד מתקדם כגון אפקטים טשטוש העלולים לגרום לבעיות ביצועים ללא עיבוד מואץ לחומרה."
},
"app.auth-servers.unreachable.body": {
"message": "ייתכן ששרתי האימות של Minecraft מושבתים כרגע. יש לבדוק את חיבור האינטרנט שלך ולנסות שוב מאוחר יותר."
},
@@ -254,9 +266,6 @@
"friends.action.add-friend": {
"message": "הוספת חבר"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {בקשת חברות אחת} other {# בקשות חברות}}"
},
"friends.add-friend.submit": {
"message": "שליחת בקשת חברות"
},
+274 -16
View File
@@ -6,29 +6,92 @@
"message": "Letöltések"
},
"app.action-bar.hide-more-running-instances": {
"message": "További futó profilok elrejtése"
"message": "További futó példányok elrejtése"
},
"app.action-bar.make-primary-instance": {
"message": "Beállítás elsődleges profilként"
"message": "Beállítás elsődleges példányként"
},
"app.action-bar.no-instances-running": {
"message": "Nincsenek futó profilok"
"message": "Nincsenek futó példányok"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Elsődleges profil"
"message": "Elsődleges példány"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó pédányok megjelenítése"
},
"app.action-bar.stop-instance": {
"message": "Profil megállítása"
"message": "Példány leállítása"
},
"app.action-bar.view-active-downloads": {
"message": "Aktív letöltések megtekintése"
},
"app.action-bar.view-instance": {
"message": "Példányok megtekintése"
},
"app.action-bar.view-logs": {
"message": "Naplók megtekintése"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Lehetővé teszi olyan speciális megjelenítési effektusok használatát, mint például az elmosódás, amelyek hardveres gyorsítás nélkül teljesítménycsökkenést okozhatnak."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Fejlett renderelés"
},
"app.appearance-settings.color-theme.description": {
"message": "Válaszd ki a Modrinth Apphoz a kívánt színsémát."
},
"app.appearance-settings.color-theme.title": {
"message": "Téma"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Állítsd be, hogy az indító melyik oldalon nyíljon meg."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Kezdőlap"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Könyvtár"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Alapértelmezett nyitóoldal"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Letiltja a játékosod felett megjelenő névcímkét a Kinézetek oldalon."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Névcímke eltüntetése"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "A kezdőlap „Folytasd itt” részében a legújabb világok is megtalálhatók."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "„Folytasd itt” a világokat is"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizáld az indítót a Minecraft indításakor."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Indító minimalizálása"
},
"app.appearance-settings.native-decorations.description": {
"message": "A rendszer ablakkeret használata (az alkalmazás újraindítása szükséges)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Rendszer ablakkeret használata"
},
"app.appearance-settings.select-option": {
"message": "Válassz egy lehetőséget"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Lehetővé teszi az oldalsáv be- és kikapcsolását."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Oldalsáv megjelenítése/elrejtése"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Ha olyan Modrinth Csomagfájlt (.mrpack) próbálsz telepíteni, amely nem a Modrinth szerverén található, a telepítés előtt gondoskodunk arról, hogy tisztában legyél a kockázatokkal."
},
@@ -41,8 +104,14 @@
"app.auth-servers.unreachable.header": {
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-servers-to-instance": {
"message": "Szerver hozzáadása az példányhoz"
},
"app.browse.add-to-an-instance": {
"message": "Hozzáadás egy példányhoz"
},
"app.browse.add-to-instance": {
"message": "Hozzáadás a profilhoz"
"message": "Példányhoz adás"
},
"app.browse.add-to-instance-name": {
"message": "Hozzáadás ehhez: {instanceName}"
@@ -54,7 +123,7 @@
"message": "Már hozzá van adva"
},
"app.browse.back-to-instance": {
"message": "Vissza a profilhoz"
"message": "Vissza a példányhoz"
},
"app.browse.discover-content": {
"message": "Tartalom böngészése"
@@ -68,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Modcsomagok"
},
"app.browse.server-instance-content-warning": {
"message": "A tartalom hozzáadása ronthatja a kompatibilitást a szerverhez való csatlakozáskor. A hozzáadott tartalom elveszik, ha frissíted a szerverprofil tartalmát."
},
"app.browse.server.installing": {
"message": "Letöltés..."
},
@@ -86,12 +158,18 @@
"app.export-modal.header": {
"message": "Modcsomag exportálása"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Tartalmaz „{file}” fájlokat?"
},
"app.export-modal.modpack-name-label": {
"message": "A modcsomag neve"
},
"app.export-modal.modpack-name-placeholder": {
"message": "A modcsomag neve"
},
"app.export-modal.select-files-label": {
"message": "Állítsd be, hogy mely fájlok kerüljenek bele az exportba"
},
"app.export-modal.version-number-label": {
"message": "Verziószám"
},
@@ -120,7 +198,7 @@
"message": "A modcsomag már telepítve van"
},
"app.instance.modpack-already-installed.instance": {
"message": "Profil"
"message": "Példány"
},
"app.instance.mods.content-type-project": {
"message": "projekt"
@@ -194,9 +272,6 @@
"app.modal.install-to-play.install-button": {
"message": "Telepítés"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Szükséges modcsomag"
},
@@ -219,7 +294,10 @@
"message": "Frissítés szükséges"
},
"app.modal.update-to-play.update-required-description": {
"message": "\nFrissítés szükséges ehhez: {name}. Kérk, frissíts a legújabb verzióra a játék elindításához"
"message": "Frissítés szükséges ehhez: {name}. Kérlek, frissíts a legújabb verzióra a játék elindításához."
},
"app.project.install-button.already-installed": {
"message": "Ez a projekt már telepítve van"
},
"app.project.install-context.back-to-browse": {
"message": "Vissza a böngészéshez"
@@ -251,6 +329,138 @@
"app.settings.tabs.resource-management": {
"message": "Erőforráskezelés"
},
"app.skins.add-button": {
"message": "Kinézet hozzáadása"
},
"app.skins.add-button.drag-and-drop": {
"message": "Húzd ide a fájlt"
},
"app.skins.apply-button": {
"message": "Alkalmazás"
},
"app.skins.delete-button": {
"message": "Kinézet törlése"
},
"app.skins.delete-modal.description": {
"message": "Ezzel a kiválasztott kinézet véglegesen törlődik. Ez a művelet visszafordíthatatlan."
},
"app.skins.delete-modal.title": {
"message": "Biztos, hogy le akarod törölni ezt a kinézetet?"
},
"app.skins.dropped-file-error.text": {
"message": "Nem sikerült beolvasni a feltöltött fájlt."
},
"app.skins.dropped-file-error.title": {
"message": "Hiba a fájl feldolgozása közben"
},
"app.skins.edit-button": {
"message": "Kinézet szerkesztése"
},
"app.skins.modal.add-skin-button": {
"message": "Kinézet hozzáadása"
},
"app.skins.modal.add-title": {
"message": "Kinézet hozzáadása"
},
"app.skins.modal.arm-style-section": {
"message": "Karstílus"
},
"app.skins.modal.arm-style-slim": {
"message": "Vékony"
},
"app.skins.modal.arm-style-wide": {
"message": "Széles"
},
"app.skins.modal.cape-fallback-name": {
"message": "Köpeny"
},
"app.skins.modal.cape-section": {
"message": "Köpeny"
},
"app.skins.modal.edit-title": {
"message": "Kinézet szerkesztése"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Előbb módosíts valamit!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Nincs köpeny"
},
"app.skins.modal.none-cape-option": {
"message": "Nincs"
},
"app.skins.modal.replace-texture-button": {
"message": "Textúra cseréje"
},
"app.skins.modal.save-skin-button": {
"message": "Kinézet mentése"
},
"app.skins.modal.saving-tooltip": {
"message": "Mentés..."
},
"app.skins.modal.texture-section": {
"message": "Textúra"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Először tölts fel egy kinézetet!"
},
"app.skins.preview.edit-button": {
"message": "Kinézet szerkesztése"
},
"app.skins.previewing-badge": {
"message": "Előnézet"
},
"app.skins.rate-limit.text": {
"message": "Túl gyakran cseréled a kinézetedet. A Mojang szerverei ideiglenesen letiltották a további cseréket. Kérlek várj egy pillanatot, majd próbáld meg újra."
},
"app.skins.rate-limit.title": {
"message": "Lassíts!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Alap kinézetek"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Mentett kinézetek"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Bejelentkezés"
},
"app.skins.sign-in.description": {
"message": "Kérlek jelentkezz be a Minecraft-fiókodba, hogy használni tudd a Modrinth App kinézetkezelési funkcióit."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Izgatott Modrinth-bot"
},
"app.skins.sign-in.title": {
"message": "Kérlek jelentkezz be"
},
"app.skins.title": {
"message": "Kinézetváltó"
},
"app.update-popup.body": {
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
},
@@ -314,9 +524,6 @@
"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"
},
@@ -530,6 +737,24 @@
"instance.settings.tabs.java": {
"message": "Java és memória"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Egyéni környezeti változók"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Egyéni Java argumentumok"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Egyedi Java telepítés"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Egyéni memóriakiosztás"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Írja be a környezeti változókat..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java argumentumok megadása..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Környezeti változók"
},
@@ -545,6 +770,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Hozzárendelt memória"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Ablak"
},
@@ -588,7 +816,7 @@
"message": "Ne mutasd a kezdőlapon"
},
"instance.worlds.game_already_open": {
"message": "A profil már meg van nyitva"
"message": "A profil már el van indítva"
},
"instance.worlds.hardcore": {
"message": "Hardcore mód"
@@ -617,6 +845,24 @@
"instance.worlds.world_in_use": {
"message": "A világ használatban van"
},
"minecraft-account.add-account": {
"message": "Fiók hozzáadása"
},
"minecraft-account.label": {
"message": "Minecraft-fiók"
},
"minecraft-account.not-signed-in": {
"message": "Nem vagy bejelentkezve"
},
"minecraft-account.remove-account": {
"message": "Fiók törlése"
},
"minecraft-account.select-account": {
"message": "Fiók kiválasztása"
},
"minecraft-account.sign-in": {
"message": "Bejelentkezés a Minecraftba"
},
"search.filter.locked.instance": {
"message": "A profil által van megadva"
},
@@ -641,13 +887,25 @@
"search.filter.locked.server-loader.title": {
"message": "A betöltőt a szerver biztosítja"
},
"unknown-pack-warning-modal.body": {
"message": "Egy fájlt csak akkor vizsgálunk meg, ha azt feltöltik a Modrinthra, függetlenül a fájlformátumtól (beleértve a .mrpack formátumot is)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Ne mutasd újra ezt a figyelmeztetést"
},
"unknown-pack-warning-modal.header": {
"message": "Telepítés megerősítése"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Letöltés mindenképpen"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "A rosszindulatú programokat gyakran modcsomag-fájlokon keresztül terjesztik, például a Discordhoz hasonló platformokon történő megosztás révén."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ezt a fájlt nem találtuk meg a Modrinthon. Határozottan javasoljuk, hogy kizárólag megbízható forrásokból származó fájlokat telepíts."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Figyelmeztetés ismeretlen fájlról"
}
}
+240 -6
View File
@@ -56,21 +56,48 @@
"app.appearance-settings.default-landing-page.library": {
"message": "Pustaka"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Halaman arahan bawaan"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Mematikan label nama di atas wujud pemain Anda pada halaman rupa."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Sembunyikan label nama"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Menyertakan dunia terkini dalam bagian \"Masuk kembali\" pada Beranda."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Masuk kembali ke dunia"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Kecilkan pluncur ketika proses Minecraft berjalan."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Kecilkan peluncur"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gunakan bingkai jendela sistem (wajib menjalankan ulang aplikasi)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Menghidupkan kemampuan untuk menghidupkan dan mematikan bilah sisi."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Hidup/matikan bilah sisi"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Bila Anda mencoba memasangkan berkas Modrinth Pack (.mrpack) yang tidak dihoskan pada Modrinth, kami akan memastikan Anda untuk memahami risikonya sebelum memasangkannya."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Peringatkan saya sebelum memasang paket mod tidak dikenal"
},
"app.auth-servers.unreachable.body": {
"message": "Server autentikasi Minecraft mungkin sedang tidak tersedia saat ini. Periksa koneksi internet Anda dan coba lagi nanti."
},
@@ -110,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Paket Mod"
},
"app.browse.server-instance-content-warning": {
"message": "Menambah konten dapat merusak kompatibilitas saat bergabung ke server. Konten tambahan apa pun akan hilang ketika Anda memperbarui konten instans server."
},
"app.browse.server.installing": {
"message": "Memasang"
},
@@ -242,9 +272,6 @@
"app.modal.install-to-play.install-button": {
"message": "Pasang"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Paket mod yang diperlukan"
},
@@ -269,6 +296,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name} perlu diperbarui sebelum dimainkan. Mohon perbarui ke versi terkini untuk meluncurkan permainan."
},
"app.project.install-button.already-installed": {
"message": "Proyek ini sudah terpasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali ke halaman pencarian"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang konten ke instans"
},
"app.settings.developer-mode-enabled": {
"message": "Mode pengembang dihidupkan."
},
@@ -293,6 +329,147 @@
"app.settings.tabs.resource-management": {
"message": "Manajemen sumber"
},
"app.skins.add-button": {
"message": "Tambah rupa"
},
"app.skins.add-button.drag-and-drop": {
"message": "Seret dan lepas"
},
"app.skins.apply-button": {
"message": "Terapkan"
},
"app.skins.delete-button": {
"message": "Hapus rupa"
},
"app.skins.delete-modal.description": {
"message": "Ini akan menghapus rupa terpilih secara permanen. Tindakan ini tidak dapat dibatalkan."
},
"app.skins.delete-modal.title": {
"message": "Apakah Anda yakin ingin menghapus rupa ini?"
},
"app.skins.dropped-file-error.text": {
"message": "Gagal membaca berkas yang diletakkan."
},
"app.skins.dropped-file-error.title": {
"message": "Kesalahan dalam memroses berkas"
},
"app.skins.edit-button": {
"message": "Sunting rupa"
},
"app.skins.modal.add-skin-button": {
"message": "Tambah rupa"
},
"app.skins.modal.add-title": {
"message": "Menambah rupa"
},
"app.skins.modal.arm-style-section": {
"message": "Gaya lengan"
},
"app.skins.modal.arm-style-slim": {
"message": "Ramping"
},
"app.skins.modal.arm-style-wide": {
"message": "Lebar"
},
"app.skins.modal.cape-fallback-name": {
"message": "Jubah"
},
"app.skins.modal.cape-section": {
"message": "Jubah"
},
"app.skins.modal.edit-title": {
"message": "Menyunting rupa"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Sunting rupa terlebih dahulu!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Tanpa jubah"
},
"app.skins.modal.none-cape-option": {
"message": "Tidak ada"
},
"app.skins.modal.replace-texture-button": {
"message": "Ganti tekstur"
},
"app.skins.modal.save-skin-button": {
"message": "Simpan rupa"
},
"app.skins.modal.saving-tooltip": {
"message": "Menyimpan..."
},
"app.skins.modal.texture-section": {
"message": "Tekstur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Unggah rupa terlebih dahulu!"
},
"app.skins.preview.edit-button": {
"message": "Sunting rupa"
},
"app.skins.previewing-badge": {
"message": "Mempratinjau"
},
"app.skins.rate-limit.text": {
"message": "Anda terlalu sering mengganti rupa Anda. Server Mojang telah memblokir permintaan lebih lanjut untuk sementara. Mohon tunggu sebentar sebelum mencoba lagi."
},
"app.skins.rate-limit.title": {
"message": "Pelan-pelan!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Rupa bawaan"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Anda mendapat rupa ini karena telah berdonasi ke penggalangan dana Modrinth Pride saat Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Rupa tersimpan"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Masuk"
},
"app.skins.sign-in.description": {
"message": "Mohon masuk ke akun Minecraft Anda untuk menggunakan fitur pengelolaan rupa dari Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot Modrinth penuh antusias"
},
"app.skins.sign-in.title": {
"message": "Silakan masuk"
},
"app.skins.title": {
"message": "Pemilih rupa"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} siap dipasang! Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
@@ -356,9 +533,6 @@
"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"
},
@@ -572,6 +746,24 @@
"instance.settings.tabs.java": {
"message": "Java dan memori"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variabel lingkungan tersuai"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumen Java tersuai"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Pemasangan Java tersuai"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Alokasi memori tersuai"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Masukkan variabel lingkungan..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Masukkan argumen Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variabel lingkungan"
},
@@ -587,6 +779,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memori yang teralokasikan"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/jalur/ke/java"
},
"instance.settings.tabs.window": {
"message": "Jendela"
},
@@ -659,6 +854,24 @@
"instance.worlds.world_in_use": {
"message": "Dunia sedang dimainkan"
},
"minecraft-account.add-account": {
"message": "Tambah akun"
},
"minecraft-account.label": {
"message": "Akun Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Tidak masuk"
},
"minecraft-account.remove-account": {
"message": "Hapus akun"
},
"minecraft-account.select-account": {
"message": "Pilih akun"
},
"minecraft-account.sign-in": {
"message": "Masuk ke Minecraft"
},
"search.filter.locked.instance": {
"message": "Disediakan oleh instans"
},
@@ -682,5 +895,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Pemuat disediakan oleh server"
},
"unknown-pack-warning-modal.body": {
"message": "Berkas hanya akan ditinjau bila ia diunggah ke Modrinth, tak peduli format berkasnya (termasuk .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Jangan tampilkan peringatan ini lagi"
},
"unknown-pack-warning-modal.header": {
"message": "Konfirmasi pemasangan"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Tetap pasang"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Program jahat (malware) dibagikan melalui berkas paket mod dengan membagikannya melalui platform seperti Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Kami tidak dapat menemukan berkas ini di Modrinth. Kami sangat menyarankan Anda untuk hanya memasang berkas dari sumber-sumber terpercaya."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Peringatan berkas tidak dikenal"
}
}
+165 -15
View File
@@ -84,7 +84,7 @@
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Scegli una pagina"
"message": "Seleziona"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Scegli se mostrare o nascondere la barra laterale."
@@ -93,7 +93,7 @@
"message": "Abilita la barra laterale"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Visti i rischi nell'installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, mostriamo un avvertimento quando tenti di fare ciò."
"message": "Visti i rischi, apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami se installo pacchetti sconosciuti"
@@ -224,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Esplora i server"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" verrà **eliminato permanentemente** e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.delete-world-title": {
"message": "Vuoi davvero eliminare questo mondo per sempre?"
},
@@ -248,12 +245,6 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Nessun server o mondo aggiunto"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.remove-server-title": {
"message": "Vuoi davvero rimuovere {name}?"
},
@@ -272,9 +263,6 @@
"app.modal.install-to-play.install-button": {
"message": "Installa"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pacchetto di mod richiesto"
},
@@ -303,7 +291,7 @@
"message": "Questo progetto è già stato installato"
},
"app.project.install-context.back-to-browse": {
"message": "Torna su esplora"
"message": "Torna a Esplora"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installa il contenuto nell'istanza"
@@ -332,6 +320,147 @@
"app.settings.tabs.resource-management": {
"message": "Gestione delle risorse"
},
"app.skins.add-button": {
"message": "Nuova skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Trascina e rilascia"
},
"app.skins.apply-button": {
"message": "Applica"
},
"app.skins.delete-button": {
"message": "Elimina"
},
"app.skins.delete-modal.description": {
"message": "Questa skin sarà rimossa per sempre. Quest'azione non può essere annullata."
},
"app.skins.delete-modal.title": {
"message": "Vuoi davvero eliminare questa skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Non è stato possibile leggere il file rilasciato."
},
"app.skins.dropped-file-error.title": {
"message": "Impossibile processare il file"
},
"app.skins.edit-button": {
"message": "Modifica"
},
"app.skins.modal.add-skin-button": {
"message": "Salva"
},
"app.skins.modal.add-title": {
"message": "Aggiungi una nuova skin"
},
"app.skins.modal.arm-style-section": {
"message": "Modello del giocatore"
},
"app.skins.modal.arm-style-slim": {
"message": "Snello"
},
"app.skins.modal.arm-style-wide": {
"message": "Largo"
},
"app.skins.modal.cape-fallback-name": {
"message": "Mantello"
},
"app.skins.modal.cape-section": {
"message": "Mantello"
},
"app.skins.modal.edit-title": {
"message": "Modifica la skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Modifica la skin prima!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Nessun mantello"
},
"app.skins.modal.none-cape-option": {
"message": "Nessuno"
},
"app.skins.modal.replace-texture-button": {
"message": "Sfoglia"
},
"app.skins.modal.save-skin-button": {
"message": "Salva"
},
"app.skins.modal.saving-tooltip": {
"message": "Salvataggio..."
},
"app.skins.modal.texture-section": {
"message": "File della skin"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Carica una skin prima!"
},
"app.skins.preview.edit-button": {
"message": "Modifica"
},
"app.skins.previewing-badge": {
"message": "Anteprima"
},
"app.skins.rate-limit.text": {
"message": "Stai modificando la skin troppo spesso. I server di Mojang hanno limitato le nostre richieste. Aspetta un momento prima di riprovare."
},
"app.skins.rate-limit.title": {
"message": "Rallenta!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Skin predefinite"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Pride Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Hai ottenuto queste skin per aver donato alla raccolta fondi per il Pride durante il Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Raccolta"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Accedi"
},
"app.skins.sign-in.description": {
"message": "Accedi col tuo account Minecraft per usare la gestione delle skin."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emozionato"
},
"app.skins.sign-in.title": {
"message": "Accedi"
},
"app.skins.title": {
"message": "Seleziona una skin"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} è pronta per essere installata! Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
},
@@ -611,6 +740,24 @@
"instance.settings.tabs.java": {
"message": "Java e memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variabili d'ambiente"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argomenti JVM"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Eseguibile di Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Memoria allocata"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Inserisci le variabili d'ambiente..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Inserisci gli argomenti JVM..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variabili d'ambiente"
},
@@ -626,6 +773,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memoria allocata"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/percorso/di/java"
},
"instance.settings.tabs.window": {
"message": "Finestra"
},
@@ -254,9 +254,6 @@
"app.modal.install-to-play.install-button": {
"message": "インストール"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {#個のMod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "必須のModパック"
},
@@ -368,9 +365,6 @@
"friends.action.add-friend": {
"message": "フレンドを追加"
},
"friends.action.view-friend-requests": {
"message": "{count}件の友達リクエスト"
},
"friends.add-friend.submit": {
"message": "フレンド申請を送信"
},
+174 -6
View File
@@ -24,7 +24,7 @@
"message": "실행 중인 인스턴스 보이기"
},
"app.action-bar.stop-instance": {
"message": "인스턴스 중지"
"message": "인스턴스 멈추기"
},
"app.action-bar.view-active-downloads": {
"message": "다운로드 목록 보기"
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "인증 서버에 연결할 수 없습니다"
},
"app.browse.add-servers-to-instance": {
"message": "인스턴스에 서버 추가"
},
"app.browse.add-to-an-instance": {
"message": "인스턴스에 추가"
},
"app.browse.add-to-instance": {
"message": "인스턴스에 추가"
},
@@ -116,6 +122,9 @@
"app.browse.already-added": {
"message": "이미 추가됨"
},
"app.browse.back-to-instance": {
"message": "인스턴스로 돌아가기"
},
"app.browse.discover-content": {
"message": "콘텐츠 탐색하기"
},
@@ -128,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "모드팩"
},
"app.browse.server-instance-content-warning": {
"message": "콘텐츠를 추가하면 서버 접속 시 호환이 안 될 수 있습니다. 또한 서버 인스턴스 콘텐츠를 업데이트하면 임의로 추가된 콘텐츠는 모두 손실됩니다."
},
"app.browse.server.installing": {
"message": "설치 중"
},
@@ -180,10 +192,10 @@
"message": "이 모드팩은 이미 <bold>{instanceName}</bold> 인스턴스에 설치되어 있습니다. 정말로 복제하시겠습니까?"
},
"app.instance.modpack-already-installed.create": {
"message": "생성"
"message": "만들기"
},
"app.instance.modpack-already-installed.header": {
"message": "모드팩 이미 설치"
"message": "이미 설치된 모드팩"
},
"app.instance.modpack-already-installed.instance": {
"message": "인스턴스"
@@ -287,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name}을(를) 플레이하려면 업데이트가 필요합니다. 게임을 실행하려면 최신 버전으로 업데이트해 주세요."
},
"app.project.install-button.already-installed": {
"message": "이미 설치되어 있음"
},
"app.project.install-context.back-to-browse": {
"message": "탐색으로 돌아가기"
},
"app.project.install-context.install-content-to-instance": {
"message": "인스턴스에 콘텐츠 설치"
},
"app.settings.developer-mode-enabled": {
"message": "개발자 모드가 활성화되었습니다."
},
@@ -311,6 +332,135 @@
"app.settings.tabs.resource-management": {
"message": "리소스 관리"
},
"app.skins.add-button": {
"message": "스킨 추가"
},
"app.skins.add-button.drag-and-drop": {
"message": "드레그 엔 드롭"
},
"app.skins.apply-button": {
"message": "적용"
},
"app.skins.delete-button": {
"message": "스킨 지우기"
},
"app.skins.delete-modal.description": {
"message": "선택한 스킨이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다."
},
"app.skins.delete-modal.title": {
"message": "이 스킨을 정말 삭제하시겠습니까?"
},
"app.skins.dropped-file-error.text": {
"message": "삭제된 파일을 읽는 데 실패했습니다."
},
"app.skins.dropped-file-error.title": {
"message": "파일 처리 중 오류가 발생했습니다."
},
"app.skins.edit-button": {
"message": "스킨 편집"
},
"app.skins.modal.add-skin-button": {
"message": "스킨 추가"
},
"app.skins.modal.add-title": {
"message": "스킨 추가"
},
"app.skins.modal.arm-style-section": {
"message": "팔 형테"
},
"app.skins.modal.arm-style-slim": {
"message": "Slim"
},
"app.skins.modal.arm-style-wide": {
"message": "Wide"
},
"app.skins.modal.cape-fallback-name": {
"message": "망토"
},
"app.skins.modal.cape-section": {
"message": "망토"
},
"app.skins.modal.edit-title": {
"message": "스킨 수정"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "먼저 스킨을 수정하세요"
},
"app.skins.modal.no-cape-tooltip": {
"message": "망토 없음"
},
"app.skins.modal.none-cape-option": {
"message": "망토 없음"
},
"app.skins.modal.replace-texture-button": {
"message": "찾아보기"
},
"app.skins.modal.save-skin-button": {
"message": "스킨 저장하기"
},
"app.skins.modal.saving-tooltip": {
"message": "저장중..."
},
"app.skins.modal.texture-section": {
"message": "스킨 파일"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "먼저 스킨을 업로드하세요"
},
"app.skins.preview.edit-button": {
"message": "스킨 수정"
},
"app.skins.previewing-badge": {
"message": "미리보기"
},
"app.skins.rate-limit.text": {
"message": "스킨을 너무 자주 변경하고 있습니다. Mojang 서버에서 일시적으로 추가 요청을 차단했습니다. 잠시 후 다시 시도해 주세요."
},
"app.skins.rate-limit.title": {
"message": "잠시 기다려주세요."
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "기본 스킨"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "저장된 스킨"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "로그인"
},
"app.skins.sign-in.description": {
"message": "Modrinth 앱의 스킨 관리 기능을 사용하려면 Minecraft 계정에 로그인하세요."
},
"app.skins.sign-in.title": {
"message": "로그인하세요"
},
"app.skins.title": {
"message": "스킨 선택"
},
"app.update-popup.body": {
"message": "Modrinth App v{version}을 설치할 준비가 완료되었습니다! 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트됩니다."
},
@@ -374,9 +524,6 @@
"friends.action.add-friend": {
"message": "친구 추가"
},
"friends.action.view-friend-requests": {
"message": "{count}건의 친구 요청"
},
"friends.add-friend.submit": {
"message": "친구 요청 보내기"
},
@@ -590,6 +737,24 @@
"instance.settings.tabs.java": {
"message": "Java 및 메모리"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "사용자 지정 환경 변수"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "사용자 지정 Java 매개변수"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "사용자 지정 Java 설치"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "사용자 지정 메모리 할당"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "환경 변수를 입력하세요..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java 인수를 입력하세요..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "환경 변수"
},
@@ -605,6 +770,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "할당된 메모리"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "창"
},
+19 -7
View File
@@ -272,9 +272,6 @@
"app.modal.install-to-play.install-button": {
"message": "Pasang"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pek mod yang diperlukan"
},
@@ -303,7 +300,7 @@
"message": "Projek ini sudah dipasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali melayar"
"message": "Kembali menemukan"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
@@ -395,9 +392,6 @@
"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"
},
@@ -611,6 +605,21 @@
"instance.settings.tabs.java": {
"message": "Java dan ingatan"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Pembolehubah persekitaran tersuai"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumen Java tersuai"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Pemasangan Java tersuai"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Peruntukan ingatan tersuai"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Masukkan argumen Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Pembolehubah persekitaran"
},
@@ -626,6 +635,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Ingatan yang diperuntukkan"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/laluan/ke/java"
},
"instance.settings.tabs.window": {
"message": "Tetingkap"
},
+1 -10
View File
@@ -189,7 +189,7 @@
"message": "Verwijder instantie"
},
"app.instance.modpack-already-installed.body": {
"message": "Deze modpakket is al in de <bold>{instanceName}<bold> instantie geïnstalleerd. Ben je zeker dat je het geen wil dupliceren?"
"message": "Deze modpakket is al in de <bold>{instanceName}</bold> instantie geïnstalleerd. Ben je zeker dat je het geen wil dupliceren?"
},
"app.instance.modpack-already-installed.create": {
"message": "Maak"
@@ -224,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "Zoek servers"
},
"app.instance.worlds.delete-world-description": {
"message": "{name} Zal **permanent verwijderd** worden en kan niet hersteld worden."
},
"app.instance.worlds.delete-world-title": {
"message": "Weet je zeker dat je deze wereld definitief wilt verwijderen?"
},
@@ -257,9 +254,6 @@
"app.instance.worlds.remove-server-title": {
"message": "Ben je zeker dat je {name} wil verwijderen?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Zoek werelden"
},
"app.instance.worlds.this-server": {
"message": "deze server"
},
@@ -302,9 +296,6 @@
"app.project.install-button.already-installed": {
"message": "Dit project is al geïnstalleerd"
},
"app.project.install-context.back-to-browse": {
"message": "Terug naar browsen"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installeer content naar instantie"
},
+98 -11
View File
@@ -137,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Paczki modów"
},
"app.browse.server-instance-content-warning": {
"message": "Dodawanie zawartości może zepsuć kompatybilność podczas dołączania na serwer. Jakakolwiek dodana zawartość zostanie stracona, gdy zaktualizujesz zawartość instancji serwera."
},
"app.browse.server.installing": {
"message": "Instalowanie"
},
@@ -155,12 +158,18 @@
"app.export-modal.header": {
"message": "Eksportuj paczkę modów"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Dołączyć \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nazwa paczki modów"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Nazwa paczki modów"
},
"app.export-modal.select-files-label": {
"message": "Skonfiguruje jakie pliki zostaną załączone w tym eksporcie"
},
"app.export-modal.version-number-label": {
"message": "Numer wersji"
},
@@ -168,7 +177,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i wszystkie zainstalowane treści."
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i jakakolwiek dodana zawartość."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Tej akcji nie można odwrócić"
@@ -197,9 +206,6 @@
"app.instance.mods.project-was-added": {
"message": "Dodano \"{name}\""
},
"app.instance.mods.projects-were-added": {
"message": "Dodano {count} {count, plural, one {projekt} few {projekty} other {projektów}}"
},
"app.instance.mods.share-text": {
"message": "Sprawdź projekty, które używam w mojej paczce modów!"
},
@@ -263,9 +269,6 @@
"app.modal.install-to-play.install-button": {
"message": "Zainstaluj"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} few {# mody} other {# modów}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Wymagana paczka modów"
},
@@ -294,7 +297,7 @@
"message": "Ten projekt jest już zainstalowany"
},
"app.project.install-context.back-to-browse": {
"message": "Powrót do przeglądania"
"message": "Powróć do odkrywania"
},
"app.project.install-context.install-content-to-instance": {
"message": "Zainstaluj zasoby do instancji"
@@ -323,6 +326,69 @@
"app.settings.tabs.resource-management": {
"message": "Zarządzanie zasobami"
},
"app.skins.add-button": {
"message": "Dodaj skina"
},
"app.skins.apply-button": {
"message": "Zastosuj"
},
"app.skins.delete-button": {
"message": "Usuń skina"
},
"app.skins.edit-button": {
"message": "Edytuj skina"
},
"app.skins.modal.add-skin-button": {
"message": "Dodaj skina"
},
"app.skins.modal.add-title": {
"message": "Dodawanie skina"
},
"app.skins.modal.cape-fallback-name": {
"message": "Peleryna"
},
"app.skins.modal.cape-section": {
"message": "Pelerynki"
},
"app.skins.modal.edit-title": {
"message": "Edytuj skina"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Brak peleryny"
},
"app.skins.modal.none-cape-option": {
"message": "Nic"
},
"app.skins.modal.save-skin-button": {
"message": "Zapisz skina"
},
"app.skins.modal.saving-tooltip": {
"message": "Zapisywanie..."
},
"app.skins.modal.texture-section": {
"message": "Tekstura"
},
"app.skins.preview.edit-button": {
"message": "Edytuj skina"
},
"app.skins.rate-limit.title": {
"message": "Zwolnij!"
},
"app.skins.section.default-skins": {
"message": "Domyślne skiny"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.sign-in.button": {
"message": "Zaloguj Się"
},
"app.skins.sign-in.title": {
"message": "Proszę zaloguj się"
},
"app.skins.title": {
"message": "Wybierz skina"
},
"app.update-popup.body": {
"message": "Wersja Modrinth App v{version} jest gotowa do zainstalowania! Załaduj ponownie, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
},
@@ -386,9 +452,6 @@
"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"
},
@@ -602,6 +665,24 @@
"instance.settings.tabs.java": {
"message": "Java i pamięć"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Zmienne środowiskowe"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Niestandardowe argumenty Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Niestandardowa instalacja Javy"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Niestandardowa alokacja pamięci"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Zmienne środowiskowe..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Argumenty Javy..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Zmienne środowiskowe"
},
@@ -617,6 +698,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Przypisana pamięć"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Okno"
},
@@ -695,6 +779,9 @@
"minecraft-account.label": {
"message": "Konto Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Nie zalogowano"
},
"minecraft-account.remove-account": {
"message": "Usuń konto"
},
+163 -7
View File
@@ -272,9 +272,6 @@
"app.modal.install-to-play.install-button": {
"message": "Instalar"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, =0 {Nenhum mod} one {# mod} other {# mods}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pacote de mods necessário"
},
@@ -303,7 +300,7 @@
"message": "Este projeto já foi instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Voltar ao explorar"
"message": "Voltar ao descobrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar conteúdo para a instância"
@@ -332,6 +329,147 @@
"app.settings.tabs.resource-management": {
"message": "Gerenciamento de recursos"
},
"app.skins.add-button": {
"message": "Adicionar skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Arraste e solte"
},
"app.skins.apply-button": {
"message": "Aplicar"
},
"app.skins.delete-button": {
"message": "Excluir skin"
},
"app.skins.delete-modal.description": {
"message": "Isto permanentemente deletará a skin selecionada. Esta ação não pode ser desfeita."
},
"app.skins.delete-modal.title": {
"message": "Tem certeza de que deseja excluir esta skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Falha ao ler o arquivo solto."
},
"app.skins.dropped-file-error.title": {
"message": "Erro ao processar o arquivo"
},
"app.skins.edit-button": {
"message": "Editar skin"
},
"app.skins.modal.add-skin-button": {
"message": "Adicionar skin"
},
"app.skins.modal.add-title": {
"message": "Adicionando skin"
},
"app.skins.modal.arm-style-section": {
"message": "Estilo do braço"
},
"app.skins.modal.arm-style-slim": {
"message": "Fino"
},
"app.skins.modal.arm-style-wide": {
"message": "Largo"
},
"app.skins.modal.cape-fallback-name": {
"message": "Capa"
},
"app.skins.modal.cape-section": {
"message": "Capa"
},
"app.skins.modal.edit-title": {
"message": "Editando skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Primeiro, faça uma edição na skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Sem capa"
},
"app.skins.modal.none-cape-option": {
"message": "Nenhum"
},
"app.skins.modal.replace-texture-button": {
"message": "Substituir textura"
},
"app.skins.modal.save-skin-button": {
"message": "Salvar skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Salvando..."
},
"app.skins.modal.texture-section": {
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Primeiro, faça o envio de uma skin!"
},
"app.skins.preview.edit-button": {
"message": "Editar skin"
},
"app.skins.previewing-badge": {
"message": "Pré-visualizando"
},
"app.skins.rate-limit.text": {
"message": "Você está trocando de skin com muita frequência. Os servidores da Mojang bloquearam temporariamente novas solicitações. Aguarde um momento antes de tentar novamente."
},
"app.skins.rate-limit.title": {
"message": "Vá mais devagar!"
},
"app.skins.section.builders-and-biomes": {
"message": "Construtores & Biomas"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Skins padrões"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Orgulho LGBTQIA+ do Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Você recebeu estas skins por doar para uma campanha de arrecadação de fundos do Orgulho LGBTQIA+ do Modrinth durante o Mês do Orgulho."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Skins salvas"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "A Era do Cobre"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Fazer login"
},
"app.skins.sign-in.description": {
"message": "Faça login na sua conta do Minecraft para usar os recursos de gerenciamento de skins do Modrinth app."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot animado"
},
"app.skins.sign-in.title": {
"message": "Por favor, faça login"
},
"app.skins.title": {
"message": "Seletor de skin"
},
"app.update-popup.body": {
"message": "O Modrinth App v{version} está pronto para ser instalado! Você pode recarregar para atualizar agora ou a atualização será feita automaticamente ao fechar o Modrinth App."
},
@@ -395,9 +533,6 @@
"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"
},
@@ -611,6 +746,24 @@
"instance.settings.tabs.java": {
"message": "Java e memória"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variáveis de ambiente personalizadas"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumentos Java personalizados"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Instalação personalizada do Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Alocação de memória personalizada"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Insira as variáveis de ambiente..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Insira os argumentos Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variáveis de ambiente"
},
@@ -626,6 +779,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memória alocada"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/caminho/ao/java"
},
"instance.settings.tabs.window": {
"message": "Janela"
},
+57 -9
View File
@@ -1,4 +1,61 @@
{
"app.action-bar.downloading-java": {
"message": "A descarregar a versão do java {version}"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Esconder mais instâncias a correr"
},
"app.action-bar.make-primary-instance": {
"message": "Transformar na instância principal"
},
"app.action-bar.no-instances-running": {
"message": "Nenhuma instância a correr"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Instância principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mais instâncias a correr"
},
"app.action-bar.stop-instance": {
"message": "Para instância"
},
"app.action-bar.view-active-downloads": {
"message": "Ver instâncias ativas"
},
"app.action-bar.view-instance": {
"message": "Ver instância"
},
"app.action-bar.view-logs": {
"message": "Ver logs"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderização avançada"
},
"app.appearance-settings.color-theme.description": {
"message": "Seleciona a tua cor preferida para a Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Cor de tema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Mudar a página em que o launcher abre."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Casa"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Livraria"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página de carregamento principal"
},
"app.auth-servers.unreachable.body": {
"message": "Os servidores de autenticação do Minecraft poderão estar em baixo de momento. Verifica a tua ligação à internet e tenta novamente mais tarde."
},
@@ -92,9 +149,6 @@
"app.instance.worlds.browse-servers": {
"message": "Procurar servidores"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" vai ser **apagado permanentemente**, e não haverá maneira de o recuperar."
},
"app.instance.worlds.delete-world-title": {
"message": "Tens a certeza que queres apagar permanentemente este mundo?"
},
@@ -116,12 +170,6 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Sem servidores ou mundos adicionados"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" vai ser removido da tua lista, incluindo dentro do jogo, e não haverá maneira de o recuperar."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) vai ser removido da tua lista, incluindo dentro do jogo, e não haverá maneira de o recuperar."
},
"app.instance.worlds.remove-server-title": {
"message": "Tens a certeza que queres remover {name}?"
},
+161 -29
View File
@@ -3,7 +3,7 @@
"message": "Скачивание Java {version}"
},
"app.action-bar.downloads": {
"message": "Скачивания"
"message": "Загрузки"
},
"app.action-bar.hide-more-running-instances": {
"message": "Скрыть другие активные сборки"
@@ -27,7 +27,7 @@
"message": "Остановить сборку"
},
"app.action-bar.view-active-downloads": {
"message": "Посмотреть текущие скачивания"
"message": "Посмотреть текущие загрузки"
},
"app.action-bar.view-instance": {
"message": "Посмотреть сборку"
@@ -138,7 +138,7 @@
"message": "Сборки"
},
"app.browse.server-instance-content-warning": {
"message": "Добавление контента может нарушить совместимость при подключении к серверу. Также весь добавленный контент будет удалён при обновлении содержимого серверной сборки."
"message": "Добавление контента может сломать совместимость при подключении к серверу. Весь добавленный контент также будет удалён при обновлении сервера."
},
"app.browse.server.installing": {
"message": "Установка"
@@ -206,9 +206,6 @@
"app.instance.mods.project-was-added": {
"message": "«{name}» добавлен"
},
"app.instance.mods.projects-were-added": {
"message": "{count, plural, one {Добавлен # проект} few {Добавлено # проекта} other {Добавлено # проектов}}"
},
"app.instance.mods.share-text": {
"message": "Что в моей сборке:"
},
@@ -224,9 +221,6 @@
"app.instance.worlds.browse-servers": {
"message": "Найти серверы"
},
"app.instance.worlds.delete-world-description": {
"message": "«{name}» будет **удалён навсегда**. Его невозможно восстановить."
},
"app.instance.worlds.delete-world-title": {
"message": "Вы действительно хотите удалить этот мир?"
},
@@ -248,18 +242,9 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Нет серверов и миров"
},
"app.instance.worlds.remove-server-description": {
"message": "«{name}» будет удалён из списка — в том числе в игре. Его невозможно восстановить."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "«{name}» ({address}) будет удалён из списка — в том числе в игре. Его невозможно восстановить."
},
"app.instance.worlds.remove-server-title": {
"message": "Вы действительно хотите удалить {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Поиск по {count, plural, one {# миру} other {# мирам}}..."
},
"app.instance.worlds.this-server": {
"message": "этот сервер"
},
@@ -272,9 +257,6 @@
"app.modal.install-to-play.install-button": {
"message": "Установить"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модов}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Необходимая сборка"
},
@@ -332,6 +314,141 @@
"app.settings.tabs.resource-management": {
"message": "Управление ресурсами"
},
"app.skins.add-button": {
"message": "Добавить скин"
},
"app.skins.add-button.drag-and-drop": {
"message": "Перетащите сюда"
},
"app.skins.apply-button": {
"message": "Применить"
},
"app.skins.delete-button": {
"message": "Удалить скин"
},
"app.skins.delete-modal.description": {
"message": "Выбранный скин будет удалён навсегда. Это действие необратимо."
},
"app.skins.delete-modal.title": {
"message": "Вы действительно хотите удалить этот скин?"
},
"app.skins.dropped-file-error.text": {
"message": "Не удалось прочитать перетащенный файл."
},
"app.skins.dropped-file-error.title": {
"message": "Ошибка обработки файла"
},
"app.skins.edit-button": {
"message": "Изменить скин"
},
"app.skins.modal.add-skin-button": {
"message": "Добавить скин"
},
"app.skins.modal.add-title": {
"message": "Добавление скина"
},
"app.skins.modal.arm-style-section": {
"message": "Руки"
},
"app.skins.modal.arm-style-slim": {
"message": "Тонкие"
},
"app.skins.modal.arm-style-wide": {
"message": "Широкие"
},
"app.skins.modal.cape-fallback-name": {
"message": "Плащ"
},
"app.skins.modal.cape-section": {
"message": "Плащ"
},
"app.skins.modal.edit-title": {
"message": "Настройка скина"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Сначала внесите изменения в скин!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Без плаща"
},
"app.skins.modal.none-cape-option": {
"message": "Нет"
},
"app.skins.modal.replace-texture-button": {
"message": "Заменить текстуру"
},
"app.skins.modal.save-skin-button": {
"message": "Сохранить скин"
},
"app.skins.modal.saving-tooltip": {
"message": "Сохранение..."
},
"app.skins.modal.texture-section": {
"message": "Текстура"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Сначала загрузите скин!"
},
"app.skins.preview.edit-button": {
"message": "Изменить"
},
"app.skins.previewing-badge": {
"message": "Предпросмотр"
},
"app.skins.rate-limit.text": {
"message": "Слишком частая смена скина. Серверы Mojang временно заблокировали дальнейшие запросы. Подождите немного, прежде чем попробовать снова."
},
"app.skins.rate-limit.title": {
"message": "Не так быстро!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Стандартные скины"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Сохранённые скины"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Войти"
},
"app.skins.sign-in.description": {
"message": "Чтобы использовать функции управления скинами Modrinth App, войдите в свою учётную запись Minecraft."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Восторженный Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Войдите"
},
"app.skins.title": {
"message": "Выбор скина"
},
"app.update-popup.body": {
"message": "Версия Modrinth App {version} готова к установке! Перезапустите приложение, чтобы обновить его, или оно обновится автоматически после закрытия."
},
@@ -360,7 +477,7 @@
"message": "Доступно обновление"
},
"app.update.complete-toast.text": {
"message": "Нажмите здесь, чтобы посмотреть изменения."
"message": "Нажмите, чтобы посмотреть список изменений."
},
"app.update.complete-toast.title": {
"message": "Версия {version} успешно установлена!"
@@ -395,9 +512,6 @@
"friends.action.add-friend": {
"message": "Добавить в друзья"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {запрос} few {запроса} other {запросов}} дружбы"
},
"friends.add-friend.submit": {
"message": "Отправить запрос дружбы"
},
@@ -491,9 +605,6 @@
"instance.edit-world.title": {
"message": "Настройка мира"
},
"instance.files.adding-files": {
"message": "Добавление файлов ({completed, number}/{total, number})"
},
"instance.files.save-as": {
"message": "Сохранить как..."
},
@@ -611,6 +722,24 @@
"instance.settings.tabs.java": {
"message": "Java и память"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Переопределение переменных среды"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Переопределение аргументов Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Переопределение расположения Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Переопределение выделенной памяти"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Введите переменные среды..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Введите аргументы Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Переменные среды"
},
@@ -626,6 +755,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Выделенная память"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Окно"
},
@@ -741,7 +873,7 @@
"message": "Загрузчик управляется сервером"
},
"unknown-pack-warning-modal.body": {
"message": "Файлы любого формата проверены на безопасность, если они загружены на Modrinth (в том числе файлы⠀.mrpack)."
"message": "Файл проверяется только в том случае, если он загружен на Modrinth, независимо от его формата (включая .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Больше не предупреждать"
@@ -5,12 +5,69 @@
"app.action-bar.downloads": {
"message": "Nedladdningar"
},
"app.action-bar.hide-more-running-instances": {
"message": "Göm fler pågående instanser"
},
"app.action-bar.make-primary-instance": {
"message": "Gör till primärinstans"
},
"app.action-bar.no-instances-running": {
"message": "Ingen pågående instans"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primärinstans"
},
"app.action-bar.show-more-running-instances": {
"message": "Visa fler pågående instanser"
},
"app.action-bar.stop-instance": {
"message": "Avsluta instans"
},
"app.action-bar.view-active-downloads": {
"message": "Visa aktiva nedladdningar"
},
"app.action-bar.view-instance": {
"message": "Visa instans"
},
"app.action-bar.view-logs": {
"message": "Visa loggar"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiverar avancerad rendering som till exempel oskärpa vilket kan leda till prestandaproblem utan hårdvaruaccelererad rendering."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Avancerad rendering"
},
"app.appearance-settings.color-theme.description": {
"message": "Välj färgtema för Modrinth-appen."
},
"app.appearance-settings.color-theme.title": {
"message": "Färgtema"
},
"app.appearance-settings.default-landing-page.home": {
"message": "Hem"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotek"
},
"app.appearance-settings.hide-nametag.title": {
"message": "Dölj namnskylt"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimera programstartare"
},
"app.appearance-settings.native-decorations.description": {
"message": "Använd systemets fönsterram (omstart av app krävs)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Inbyggda fönsterdekorationer"
},
"app.appearance-settings.select-option": {
"message": "Välj ett alternativ"
},
"app.auth-servers.unreachable.body": {
"message": "Minecrafts autentiseringsservrar kan vara nere just nu. Kontrollera din internetanslutning och försök igen senare."
},
@@ -41,6 +98,9 @@
"app.browse.server.installing": {
"message": "Installerar"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.export-modal.description-placeholder": {
"message": "Ange modpaketets beskrivning..."
},
@@ -50,6 +110,9 @@
"app.export-modal.header": {
"message": "Exportera modpaket"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Inkludera \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpaketets namn"
},
@@ -209,6 +272,60 @@
"app.settings.tabs.resource-management": {
"message": "Resurshantering"
},
"app.skins.add-button": {
"message": "Lägg till utseende"
},
"app.skins.delete-button": {
"message": "Ta bort utseende"
},
"app.skins.edit-button": {
"message": "Redigera utseende"
},
"app.skins.modal.add-skin-button": {
"message": "Lägg till utseende"
},
"app.skins.modal.add-title": {
"message": "Lägga till ett utseende"
},
"app.skins.modal.arm-style-section": {
"message": "Arm stil"
},
"app.skins.modal.arm-style-slim": {
"message": "Smal"
},
"app.skins.modal.arm-style-wide": {
"message": "Bred"
},
"app.skins.modal.cape-fallback-name": {
"message": "Mantel"
},
"app.skins.modal.cape-section": {
"message": "Mantel"
},
"app.skins.modal.edit-title": {
"message": "Redigerar utseende"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Gör en ändring på utseendet först!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Ingen mantel"
},
"app.skins.modal.none-cape-option": {
"message": "Ingen"
},
"app.skins.modal.replace-texture-button": {
"message": "Byt ut textur"
},
"app.skins.modal.save-skin-button": {
"message": "Spara utseende"
},
"app.skins.modal.saving-tooltip": {
"message": "Sparar..."
},
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.update-popup.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."
},
@@ -575,6 +692,24 @@
"instance.worlds.world_in_use": {
"message": "Värld används"
},
"minecraft-account.add-account": {
"message": "Lägg till konto"
},
"minecraft-account.label": {
"message": "Minecraft konto"
},
"minecraft-account.not-signed-in": {
"message": "Inte inloggad"
},
"minecraft-account.remove-account": {
"message": "Ta bort konto"
},
"minecraft-account.select-account": {
"message": "Välj konto"
},
"minecraft-account.sign-in": {
"message": "Logga in till Minecraft"
},
"search.filter.locked.instance": {
"message": "Tillhandahållet av instansen"
},
@@ -598,5 +733,8 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader tillhandahålls av servern"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Installera ändå"
}
}
@@ -260,9 +260,6 @@
"app.modal.install-to-play.install-button": {
"message": "ติดตั้ง"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# ม็อด}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "มอดแพ็กที่จำเป็น"
},
@@ -374,9 +371,6 @@
"friends.action.add-friend": {
"message": "เพิ่มเพื่อน"
},
"friends.action.view-friend-requests": {
"message": "คำขอเป็นเพื่อน {count} {count, plural, other {คน}}"
},
"friends.add-friend.submit": {
"message": "ส่งคำขอเป็นเพื่อน"
},
+180 -3
View File
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "Doğrulama sunucularına erişilemedi"
},
"app.browse.add-servers-to-instance": {
"message": "Sunucuyu kuruluma ekle"
},
"app.browse.add-to-an-instance": {
"message": "Bir kuruluma ekle"
},
"app.browse.add-to-instance": {
"message": "Kuruluma ekle"
},
@@ -116,6 +122,9 @@
"app.browse.already-added": {
"message": "Zaten ekli"
},
"app.browse.back-to-instance": {
"message": "Kuruluma dön"
},
"app.browse.discover-content": {
"message": "İçerik keşfet"
},
@@ -128,6 +137,9 @@
"app.browse.project-type.modpacks": {
"message": "Mod paketleri"
},
"app.browse.server-instance-content-warning": {
"message": "Yeni bir içerik eklemek sunucuya girerkenki uyumluluğu bozabilir. Herhangi eklenmiş içerik ise sunucu kurulumunu güncellemeye çalıştığınızda kaybolacaktır."
},
"app.browse.server.installing": {
"message": "Yükleniyor"
},
@@ -146,12 +158,18 @@
"app.export-modal.header": {
"message": "Mod paketini dışa aktar"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\" dahil mi?"
},
"app.export-modal.modpack-name-label": {
"message": "Mod Paketi Adı"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketi adı"
},
"app.export-modal.select-files-label": {
"message": "Bu dışa aktarmaya hangi dosyaların dahil edileceğini yapılandırma"
},
"app.export-modal.version-number-label": {
"message": "Sürüm numarası"
},
@@ -281,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name} oyununu oynamak için güncelleme gereklidir. Oyunu başlatmak için lütfen en son sürüme güncelleyin."
},
"app.project.install-button.already-installed": {
"message": "Bu proje zaten kurulu"
},
"app.project.install-context.back-to-browse": {
"message": "Keşfete geri dön"
},
"app.project.install-context.install-content-to-instance": {
"message": "İçeriği kuruluma yükle"
},
"app.settings.developer-mode-enabled": {
"message": "Geliştirici modu açıldı."
},
@@ -305,6 +332,138 @@
"app.settings.tabs.resource-management": {
"message": "Kaynak yönetimi"
},
"app.skins.add-button": {
"message": "Skin ekle"
},
"app.skins.add-button.drag-and-drop": {
"message": "Sürükle bırak"
},
"app.skins.apply-button": {
"message": "Uygula"
},
"app.skins.delete-button": {
"message": "Skin'i sil"
},
"app.skins.delete-modal.description": {
"message": "Bu seçilmiş skin'i kalıcı olarak silecektir. Bu işlem geri alınamaz."
},
"app.skins.delete-modal.title": {
"message": "Bu skin'i silmek istediğinize gerçekten emin misiniz?"
},
"app.skins.dropped-file-error.text": {
"message": "Bıraktığınız dosya okunamadı."
},
"app.skins.dropped-file-error.title": {
"message": "Dosya işlenirken hata oluştu"
},
"app.skins.edit-button": {
"message": "Skin'i düzenle"
},
"app.skins.modal.add-skin-button": {
"message": "Skin ekle"
},
"app.skins.modal.add-title": {
"message": "Skin ekleme"
},
"app.skins.modal.arm-style-section": {
"message": "Kol stili"
},
"app.skins.modal.arm-style-slim": {
"message": "İnce"
},
"app.skins.modal.arm-style-wide": {
"message": "Klasik"
},
"app.skins.modal.cape-fallback-name": {
"message": "Pelerin"
},
"app.skins.modal.cape-section": {
"message": "Pelerin"
},
"app.skins.modal.edit-title": {
"message": "Skin düzenleme"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Önce skin'e bir düzenleme yapın!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Pelerin yok"
},
"app.skins.modal.none-cape-option": {
"message": "Hiçbiri"
},
"app.skins.modal.replace-texture-button": {
"message": "Dokuyu değiştir"
},
"app.skins.modal.save-skin-button": {
"message": "Skin'i kaydet"
},
"app.skins.modal.saving-tooltip": {
"message": "Kaydediliyor..."
},
"app.skins.modal.texture-section": {
"message": "Doku"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "İlk başta skin yükleyin!"
},
"app.skins.preview.edit-button": {
"message": "Skin'i düzenle"
},
"app.skins.previewing-badge": {
"message": "Önizleme"
},
"app.skins.rate-limit.text": {
"message": "Skin'inizi çok hızlı değiştiriyorsunuz. Mojang'ın sunucuları geçici olarak diğer istekleri engelledi. Lütfen tekrar denemeden önce biraz bekleyin."
},
"app.skins.rate-limit.title": {
"message": "Yavaşla!"
},
"app.skins.section.builders-and-biomes": {
"message": "İnşaatçılar & Biyomlar"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Varsayılan skinler"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Kaydedilen Skinler"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Giriş Yap"
},
"app.skins.sign-in.description": {
"message": "Modrinth appdeki skin yönetim özelliklerini kullanmak için lütfen Minecraft hesabınıza giriş yapın."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Heyecanlı Modrinth Bot'u"
},
"app.skins.sign-in.title": {
"message": "Lütfen giriş yapın"
},
"app.skins.title": {
"message": "Skin seçici"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} güncellemesi indirilmeye hazır! Hemen güncellemek için yeniden başlatın veya Modrinth Appi kapattığınızda otomatik olarak güncellenecek."
},
@@ -368,9 +527,6 @@
"friends.action.add-friend": {
"message": "Bir arkadaş ekle"
},
"friends.action.view-friend-requests": {
"message": "{count} arkadaşlık isteği"
},
"friends.add-friend.submit": {
"message": "Arkadaşlık isteği gönder"
},
@@ -584,6 +740,24 @@
"instance.settings.tabs.java": {
"message": "Java ve bellek"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Özel ortam değişkenleri"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Özel Java argümanları"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Özel Java kurulumu"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Özel bellek tahsisi"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Ortam değişkenlerini girin..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java argümanlarını girin..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Ortam değişkenleri"
},
@@ -599,6 +773,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Ayrılan bellek"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/java/kurulum/yolu"
},
"instance.settings.tabs.window": {
"message": "Pencere"
},
+228 -54
View File
@@ -6,49 +6,49 @@
"message": "Завантаження"
},
"app.action-bar.hide-more-running-instances": {
"message": "Приховати більше запущених екземплярів"
"message": "Приховати більше запущених профілів"
},
"app.action-bar.make-primary-instance": {
"message": "Зробити основним екземпляром"
"message": "Зробити основним профілем"
},
"app.action-bar.no-instances-running": {
"message": "Немає запущених екземплярів"
"message": "Немає запущених профілів"
},
"app.action-bar.offline": {
"message": "Офлайн"
"message": "Поза мережею"
},
"app.action-bar.primary-instance": {
"message": "Основний екземпляр"
"message": "Основний профіль"
},
"app.action-bar.show-more-running-instances": {
"message": "Показати більше запущених екземплярів"
"message": "Показати більше запущених профілів"
},
"app.action-bar.stop-instance": {
"message": "Зупинити екземпляр"
"message": "Зупинити профіль"
},
"app.action-bar.view-active-downloads": {
"message": "Переглянути активні завантаження"
},
"app.action-bar.view-instance": {
"message": "Переглянути екземпляр"
"message": "Переглянути профіль"
},
"app.action-bar.view-logs": {
"message": "Переглянути журнали"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Дозволяє розширений рендеринг, такий як ефекти розмиття, які можуть спричиняти проблеми з продуктивністю без апаратно-прискореного рендерингу."
"message": "Дозволяє розширене промальовування, як-от ефекти розмиття, які можуть спричиняти проблеми з продуктивністю без апаратно-прискореного промальовування."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Розширений рендеринг"
"message": "Розширене промальовування"
},
"app.appearance-settings.color-theme.description": {
"message": "Виберіть бажану колірну тему для програми Modrinth App."
"message": "Виберіть бажану колірну тему для Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Колірна тема"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Змініть сторінку, на якій відкривається панель запуску."
"message": "Змініть сторінку, на якій відкривається запускач."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Головна"
@@ -57,46 +57,46 @@
"message": "Бібліотека"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Цільова сторінка за замовчуванням"
"message": "Усталена цільова сторінка"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Вимикає іменний бейдж над вашим гравцем на сторінці скінів."
"message": "Вимикає ім над вашим гравцем на сторінці скінів."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Приховати бейдж з іменем"
"message": "Приховати ім'я"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Включає нещодавні світи в розділі «Повернення» на головній сторінці."
"message": "Уключає нещодавні світи в розділі «Повернутися» на головній сторінці."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Стрибни назад у світи"
"message": "Повернутися у світи"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Згорнути лаунчер під час запуску процесу Minecraft."
"message": "Згортає запускач під час запуску процесу Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Згорнути панель запуску"
"message": "Мінімізувати запускач"
},
"app.appearance-settings.native-decorations.description": {
"message": "Використовувати рамку системного вікна (потрібно перезапустити програму)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Рідні прикраси"
"message": "Системне оформлення"
},
"app.appearance-settings.select-option": {
"message": "Виберіть опцію"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Вмикає можливість перемикання бічної панелі."
"message": "Умикає можливість перемикання бокової панелі."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Перемикання бічної панелі"
"message": "Перемикання бокової панелі"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж встановлювати його."
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж установлювати його."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Попереджати мене перед встановленням невідомих модпаків"
"message": "Попередження перед установленням невідомих збірок"
},
"app.auth-servers.unreachable.body": {
"message": "Сервери автентифікації Minecraft можуть зараз не працювати. Перевірте з’єднання з інтернетом та спробуйте пізніше."
@@ -104,6 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "Не вдається зв’язатися зі серверами автентифікації"
},
"app.browse.add-servers-to-instance": {
"message": "Додавання сервера до профілю"
},
"app.browse.add-to-an-instance": {
"message": "Додати до профілю"
},
"app.browse.add-to-instance": {
"message": "Додати до профілю"
},
@@ -116,6 +122,9 @@
"app.browse.already-added": {
"message": "Уже додано"
},
"app.browse.back-to-instance": {
"message": "Назад до профілю"
},
"app.browse.discover-content": {
"message": "Дослідити вміст"
},
@@ -123,22 +132,25 @@
"message": "Дослідити сервера"
},
"app.browse.hide-added-servers": {
"message": "Скрити наявні сервери"
"message": "Сховати наявні сервери"
},
"app.browse.project-type.modpacks": {
"message": "Модпаки"
"message": "Збірки"
},
"app.browse.server-instance-content-warning": {
"message": "Додавання вмісту може порушити сумісність при приєднанні до сервера. Усі додані матеріали також будуть утрачені під час оновлення вмісту профілю сервера."
},
"app.browse.server.installing": {
"message": "Встановлення"
"message": "Установлення"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Встановлення модпаку..."
"message": "Установлення збірки…"
},
"app.export-modal.description-placeholder": {
"message": "Уведіть опис збірки..."
"message": "Уведіть опис збірки"
},
"app.export-modal.export-button": {
"message": "Експортувати"
@@ -146,12 +158,18 @@
"app.export-modal.header": {
"message": "Експортувати збірку"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Уключити «{file}»?"
},
"app.export-modal.modpack-name-label": {
"message": "Назва збірки"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Назва збірки"
},
"app.export-modal.select-files-label": {
"message": "Змініть файли, які додані до експорту"
},
"app.export-modal.version-number-label": {
"message": "Номер версії"
},
@@ -159,7 +177,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, конфігураціями та усім встановленим контентом."
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, налаштуваннями та всім установленим умістом."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Ця дія є незворотною"
@@ -204,16 +222,13 @@
"message": "Додати сервер"
},
"app.instance.worlds.browse-servers": {
"message": "Переглянути сервера"
},
"app.instance.worlds.delete-world-description": {
"message": "«{name}» буде **назавжди видалено** й у вас не буде можливости відновлення."
"message": "Переглянути сервери"
},
"app.instance.worlds.delete-world-title": {
"message": "Ви впевнені, що хочете назавжди видалити цей світ?"
},
"app.instance.worlds.filter-modded": {
"message": "Модифікований"
"message": "Модифіковано"
},
"app.instance.worlds.filter-offline": {
"message": "Поза мережею"
@@ -222,7 +237,7 @@
"message": "У мережі"
},
"app.instance.worlds.filter-vanilla": {
"message": "Ванільний"
"message": "Стандартний"
},
"app.instance.worlds.no-worlds-description": {
"message": "Додати сервер або переглянути, щоб почати"
@@ -230,17 +245,11 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Сервера або світи не додано"
},
"app.instance.worlds.remove-server-description": {
"message": "«{name}» буде видалено з вашого списку, включно в грі, і без можливости відновлення."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "«{name}» ({address}) буде видалено з вашого списку, включно в грі, і без можливости відновлення."
},
"app.instance.worlds.remove-server-title": {
"message": "Ви впевнені, що хочете видалити «{name}»?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Пошук {count} світів..."
"message": "Пошук {count} світів"
},
"app.instance.worlds.this-server": {
"message": "цей сервер"
@@ -254,9 +263,6 @@
"app.modal.install-to-play.install-button": {
"message": "Установити"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модів}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Потрібна збірка"
},
@@ -281,6 +287,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "«{name}» потребує оновлення, щоб грати. Будь ласка, оновіть гру до останньої версії, щоб запустити її."
},
"app.project.install-button.already-installed": {
"message": "Проєкт уже встановлено"
},
"app.project.install-context.back-to-browse": {
"message": "Повернутися до дослідження"
},
"app.project.install-context.install-content-to-instance": {
"message": "Установити вміст до профілю"
},
"app.settings.developer-mode-enabled": {
"message": "Увімкнено режим розробника."
},
@@ -305,6 +320,138 @@
"app.settings.tabs.resource-management": {
"message": "Керування ресурсами"
},
"app.skins.add-button": {
"message": "Додати скін"
},
"app.skins.add-button.drag-and-drop": {
"message": "Перетягніть і вставте"
},
"app.skins.apply-button": {
"message": "Застосувати"
},
"app.skins.delete-button": {
"message": "Видалити скін"
},
"app.skins.delete-modal.description": {
"message": "Це видалить вибраний скін назавжди. Ця дія не може бути скасована."
},
"app.skins.delete-modal.title": {
"message": "Чи ви впевнені що хочете видалити цей скін?"
},
"app.skins.dropped-file-error.text": {
"message": "Неможливо відкрити вибраний файл."
},
"app.skins.dropped-file-error.title": {
"message": "Помилка обробки файлу"
},
"app.skins.edit-button": {
"message": "Редагувати скін"
},
"app.skins.modal.add-skin-button": {
"message": "Додати скін"
},
"app.skins.modal.add-title": {
"message": "Додавання скіна"
},
"app.skins.modal.arm-style-section": {
"message": "Стиль рук"
},
"app.skins.modal.arm-style-slim": {
"message": "Тонкі"
},
"app.skins.modal.arm-style-wide": {
"message": "Широкі"
},
"app.skins.modal.cape-fallback-name": {
"message": "Плащ"
},
"app.skins.modal.cape-section": {
"message": "Плащ"
},
"app.skins.modal.edit-title": {
"message": "Редагування скіна"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Відредагуйте скін для початку!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Без плаща"
},
"app.skins.modal.none-cape-option": {
"message": "Без"
},
"app.skins.modal.replace-texture-button": {
"message": "Замінити текстуру"
},
"app.skins.modal.save-skin-button": {
"message": "Зберегти вигляд"
},
"app.skins.modal.saving-tooltip": {
"message": "Збереження…"
},
"app.skins.modal.texture-section": {
"message": "Текстура"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Завантажте скін для початку!"
},
"app.skins.preview.edit-button": {
"message": "Редагувати скін"
},
"app.skins.previewing-badge": {
"message": "Попередній перегляд"
},
"app.skins.rate-limit.text": {
"message": "Ви змінюєте свій скін дуже часто. Сервери Mojang тимчасово заблокували подальші запити. Зачекайте трохи перш ніж спробувати знову."
},
"app.skins.rate-limit.title": {
"message": "Шановний!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Усталені скіни"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Збережені скіни"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Увійдіть"
},
"app.skins.sign-in.description": {
"message": "Будь ласка, увійдіть у свій обліковий запис Minecraft, щоб використовувати особливості керування скінами в Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Схвильований бот Modrinth"
},
"app.skins.sign-in.title": {
"message": "Увійдіть"
},
"app.skins.title": {
"message": "Вибір скіну"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} готовий до встановлення! Перезапустіть, щоб оновити зараз. Або, оновлення буде здійснено автоматично, коли закриєте застосунок Modrinth."
},
@@ -368,9 +515,6 @@
"friends.action.add-friend": {
"message": "Додати друга"
},
"friends.action.view-friend-requests": {
"message": "{count} запит{count, plural, one { у} few {и в} many {ів у} other {у в}} друзі"
},
"friends.add-friend.submit": {
"message": "Надіслати запит у друзі"
},
@@ -420,7 +564,7 @@
"message": "Немає друзів, які збігаються з «{query}»"
},
"friends.search-friends-placeholder": {
"message": "Пошук друзів..."
"message": "Пошук друзів"
},
"friends.section.heading": {
"message": "{title} — {count}"
@@ -468,7 +612,7 @@
"message": "Додавання файлів ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Зберегти як..."
"message": "Зберегти як"
},
"instance.server-modal.address": {
"message": "Адреса"
@@ -584,6 +728,24 @@
"instance.settings.tabs.java": {
"message": "Java та пам’ять"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Власні змінні середовища"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Власні аргументи Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Власна інсталяція Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Власний розподіл пам'яті"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Уведіть змінні середовища…"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Уведіть аргументи Java…"
},
"instance.settings.tabs.java.environment-variables": {
"message": "Змінні середовища"
},
@@ -599,6 +761,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Виділена пам’ять"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/шлях/до/java"
},
"instance.settings.tabs.window": {
"message": "Вікно"
},
@@ -674,12 +839,21 @@
"minecraft-account.add-account": {
"message": "Додати обліковий запис"
},
"minecraft-account.label": {
"message": "Обліковий запис Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Не авторизовано"
},
"minecraft-account.remove-account": {
"message": "Видалити обліковий запис"
},
"minecraft-account.select-account": {
"message": "Обрати обліковий запис"
},
"minecraft-account.sign-in": {
"message": "Увійти в Minecraft"
},
"search.filter.locked.instance": {
"message": "Надано профілем"
},
@@ -711,13 +885,13 @@
"message": "Не показувати більше це попередження"
},
"unknown-pack-warning-modal.header": {
"message": "Підтвердити інсталяцію"
"message": "Підтвердити встановлення"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Усе одно інсталювати"
"message": "Усе одно встановити"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Шкідливе програмне забезпечення часто поширюють через файли модпаків, які публікуються на таких платформах, як Discord."
"message": "Шкідливе програмне забезпечення часто поширюють через файли збірок, які публікуються на таких платформах, як Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ми не змогли знайти цей файл на Modrinth. Ми рекомендуємо встановлювати файли лише з тих джерел яким ви довіряєте."
@@ -1,10 +1,115 @@
{
"app.action-bar.downloading-java": {
"message": "Đang tải xuống Java {version}"
},
"app.action-bar.downloads": {
"message": "Tải xuống"
},
"app.action-bar.hide-more-running-instances": {
"message": "Đóng các phiên bản đang chạy"
},
"app.action-bar.make-primary-instance": {
"message": "Đặt làm bản instance chính"
},
"app.action-bar.no-instances-running": {
"message": "Không có phiên bản nào đang chạy"
},
"app.action-bar.offline": {
"message": "Ngoại tuyến"
},
"app.action-bar.primary-instance": {
"message": "Bản instance chính"
},
"app.action-bar.show-more-running-instances": {
"message": "Hiện thêm các phiên bản đang chạy"
},
"app.action-bar.stop-instance": {
"message": "Dừng instance"
},
"app.action-bar.view-active-downloads": {
"message": "Xem các lượt tải xuống"
},
"app.action-bar.view-instance": {
"message": "Xem phiên bản"
},
"app.action-bar.view-logs": {
"message": "Xem logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Kích hoạt các tính năng dựng hình nâng cao như hiệu ứng làm mờ, có thể gây giảm hiệu năng nếu không có chế độ tăng tốc phần cứng."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Dựng hình nâng cao"
},
"app.appearance-settings.color-theme.description": {
"message": "Chọn màu nền ưu thích của bạn cho Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Màu chủ đề"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Thay đổi trang hiển thị khi khởi chạy launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Trang chủ"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Thư viện"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Trang mặc định"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Tắt hiển thị thẻ tên phía trên người chơi ở trang skin."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ẩn thẻ tên"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Hiển thị các thế giới gần đây trong mục \"Tiếp tục chơi\" ở Trang chủ."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Tiếp tục chơi"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Thu nhỏ launcher khi game Minecraft bắt đầu chạy."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Thu nhỏ launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Sử dụng khung cửa sổ hệ thống (yêu cầu khởi động lại ứng dụng)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Giao diện cửa sổ hệ thống"
},
"app.appearance-settings.select-option": {
"message": "Chọn một tuỳ chọn"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Bật tính năng ẩn hiển thị thanh menu bên."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Ẩn/hiện thanh bên"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Nếu bạn cố gắng cài đặt tệp Modrinth Pack (.mrpack) không được lưu trữ trên máy chủ Modrinth, chúng tôi sẽ đảm bảo bạn hiểu rõ các rủi ro trước khi cài đặt."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Cảnh báo tôi trước khi cài đặt các modpacks không rõ nguồn gốc"
},
"app.auth-servers.unreachable.body": {
"message": "Máy chủ xác thực của Minecraft có thể đang bị sập. Hãy kiểm tra kết nối Internet của bạn và thử lại sau."
},
"app.auth-servers.unreachable.header": {
"message": "Không thể kết nối đến máy chủ xác thực"
},
"app.browse.add-servers-to-instance": {
"message": "Thêm máy chủ vào phiên bản"
},
"app.browse.add-to-an-instance": {
"message": "Thêm vào phiên bản"
},
"app.browse.add-to-instance": {
"message": "Thêm vào hồ sơ"
},
@@ -17,12 +122,33 @@
"app.browse.already-added": {
"message": "Đã được thêm"
},
"app.browse.back-to-instance": {
"message": "Quay lại phiên bản"
},
"app.browse.discover-content": {
"message": "Khám phá nội dung"
},
"app.browse.discover-servers": {
"message": "Khám phá máy chủ"
},
"app.browse.hide-added-servers": {
"message": "Ẩn các server đã thêm"
},
"app.browse.project-type.modpacks": {
"message": "Modpack"
},
"app.browse.server-instance-content-warning": {
"message": "Thêm nội dung có thể gây lỗi tương thích khi tham gia máy chủ. Bất kỳ nội dung nào được thêm vào cũng sẽ bị mất khi bạn cập nhật nội dung của phiên bản máy chủ."
},
"app.browse.server.installing": {
"message": "Đang cài đặt"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Đang cài đặt modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Thêm miêu tả cho gói modpack..."
},
@@ -32,12 +158,18 @@
"app.export-modal.header": {
"message": "Xuất modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Bao gồm \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Tên modpack"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Tên modpack"
},
"app.export-modal.select-files-label": {
"message": "Cấu hình các tệp nào được bao gồm trong quá trình xuất phiên bản này"
},
"app.export-modal.version-number-label": {
"message": "Phiên bản"
},
@@ -167,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Bạn cần cập nhật {name} để có thể chơi. Vui lòng cập nhật lên bản mới nhất để khởi chạy trò chơi."
},
"app.project.install-button.already-installed": {
"message": "Dự án này đã được cài đặt"
},
"app.project.install-context.back-to-browse": {
"message": "Quay lại trang Khám Phá"
},
"app.project.install-context.install-content-to-instance": {
"message": "Cài đặt nội dung vào phiên bản"
},
"app.settings.developer-mode-enabled": {
"message": "Chế độ nhà phát triển đã được bật."
},
@@ -191,6 +332,147 @@
"app.settings.tabs.resource-management": {
"message": "Quản lý tài nguyên"
},
"app.skins.add-button": {
"message": "Skin tự tạo"
},
"app.skins.add-button.drag-and-drop": {
"message": "Kéo và thả"
},
"app.skins.apply-button": {
"message": "Áp dụng"
},
"app.skins.delete-button": {
"message": "Xóa skin"
},
"app.skins.delete-modal.description": {
"message": "Hành động này sẽ xóa skin trên vĩnh viễn. Hành động này không thể khôi phục lại."
},
"app.skins.delete-modal.title": {
"message": "Bạn hoàn toàn muốn xóa skin này chứ?"
},
"app.skins.dropped-file-error.text": {
"message": "Không đọc được file thả."
},
"app.skins.dropped-file-error.title": {
"message": "Có lỗi khi đọc file"
},
"app.skins.edit-button": {
"message": "Sửa skin"
},
"app.skins.modal.add-skin-button": {
"message": "Tạo skin"
},
"app.skins.modal.add-title": {
"message": "Tạo skin"
},
"app.skins.modal.arm-style-section": {
"message": "Độ rộng cánh tay"
},
"app.skins.modal.arm-style-slim": {
"message": "Gầy"
},
"app.skins.modal.arm-style-wide": {
"message": "Béo"
},
"app.skins.modal.cape-fallback-name": {
"message": "Cape"
},
"app.skins.modal.cape-section": {
"message": "Cape"
},
"app.skins.modal.edit-title": {
"message": "Sửa skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Sửa skin trước đã!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Không cape"
},
"app.skins.modal.none-cape-option": {
"message": "Không"
},
"app.skins.modal.replace-texture-button": {
"message": "Đổi skin"
},
"app.skins.modal.save-skin-button": {
"message": "Lưu skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Đang lưu..."
},
"app.skins.modal.texture-section": {
"message": "Skin"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Nhập skin vào trước đã!"
},
"app.skins.preview.edit-button": {
"message": "Sửa skin"
},
"app.skins.previewing-badge": {
"message": "Xem trước"
},
"app.skins.rate-limit.text": {
"message": "Bạn đang chỉnh skin quá thường xuyên. Mãy chủ của Mojang đã tạm thời chặn lại các yêu cầu hiện thời. Đợi một lúc trước khi thử lại lần nữa."
},
"app.skins.rate-limit.title": {
"message": "Bình tĩnh lại!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Skin mặc định"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Bạn đã được tặng những skin này sau khi quyên góp cho Modrinth trong tháng Lẩu Gà Bình Thuận thế giới."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Skin đã lưu"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Đăng nhập"
},
"app.skins.sign-in.description": {
"message": "Đặng nhập vào mội tài khoản Minecraft để sử dụng trình sửa Skin trong ứng dụng này."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot phấn khích"
},
"app.skins.sign-in.title": {
"message": "Đăng nhập đã"
},
"app.skins.title": {
"message": "Trình sửa skin"
},
"app.update-popup.body": {
"message": "Phiên bản v{version} của Modrinth đã được chuẩn bị để cài đặt! Khởi động lại ứng dụng để cập nhật ngay bây giờ, hoặc cập nhật tự động khi bạn đóng Modrinth."
},
@@ -470,6 +752,24 @@
"instance.settings.tabs.java": {
"message": "Java và bộ nhớ"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Tùy chọn biến môi trường"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Tùy chỉnh tham số Java"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Tùy chỉnh phiên bản cài đặt Java"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Tùy chỉnh phân bổ bộ nhớ"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Nhập biến môi trường..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Nhập tham số Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Biến môi trường"
},
@@ -485,6 +785,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Bộ nhớ phân bổ"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/đường_dẫn/tới/java"
},
"instance.settings.tabs.window": {
"message": "Cửa sổ"
},
@@ -557,6 +860,24 @@
"instance.worlds.world_in_use": {
"message": "Thế giới đang mở"
},
"minecraft-account.add-account": {
"message": "Thêm tài khoản"
},
"minecraft-account.label": {
"message": "Tài khoản Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Chưa đăng nhập"
},
"minecraft-account.remove-account": {
"message": "Xoá tài khoản"
},
"minecraft-account.select-account": {
"message": "Chọn tài khoản"
},
"minecraft-account.sign-in": {
"message": "Đăng nhập vào Minecraft"
},
"search.filter.locked.instance": {
"message": "Được cung cấp bởi phiên bản"
},
@@ -580,5 +901,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader được cung cấp bởi máy chủ"
},
"unknown-pack-warning-modal.body": {
"message": "Tệp chỉ được xem xét nếu nó được tải lên Modrinth, bất kể định dạng tệp của nó là gì (bao gồm cả .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Đừng hiển thị cảnh báo này nữa"
},
"unknown-pack-warning-modal.header": {
"message": "Xác nhận cài đặt"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Tiếp tục cài đặt"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Phần mềm độc hại thường được phát tán thông qua các tệp modpack bằng cách chia sẻ chúng trên các nền tảng như Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Chúng tôi không tìm thấy tập tin này trên Modrinth. Chúng tôi đặc biệt khuyên bạn chỉ nên cài đặt các tập tin từ các nguồn đáng tin cậy."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Cảnh báo tệp không xác định"
}
}
+159 -18
View File
@@ -63,7 +63,7 @@
"message": "在皮肤页面中禁用你玩家头顶上的名牌。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "隐藏名"
"message": "隐藏名称标签"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "在主页的“快速回到”部分包含最近的世界。"
@@ -224,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "浏览服务器"
},
"app.instance.worlds.delete-world-description": {
"message": "“{name}”将被**永久删除**,并且无法恢复。"
},
"app.instance.worlds.delete-world-title": {
"message": "你确定要永久删除这个世界吗?"
},
@@ -248,12 +245,6 @@
"app.instance.worlds.no-worlds-heading": {
"message": "未添加服务器或世界"
},
"app.instance.worlds.remove-server-description": {
"message": "将从你的列表中删除“{name}”,包括游戏内,并且无法恢复。"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "将从你的列表中删除“{name}”({address}),包括游戏内,并且无法恢复。"
},
"app.instance.worlds.remove-server-title": {
"message": "你确定要删除 {name} 吗?"
},
@@ -272,9 +263,6 @@
"app.modal.install-to-play.install-button": {
"message": "安装"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} 个模组"
},
"app.modal.install-to-play.required-modpack": {
"message": "需求整合包"
},
@@ -303,7 +291,7 @@
"message": "此项目已安装"
},
"app.project.install-context.back-to-browse": {
"message": "返回浏览器"
"message": "返回发现页"
},
"app.project.install-context.install-content-to-instance": {
"message": "将内容安装到实例中"
@@ -332,6 +320,141 @@
"app.settings.tabs.resource-management": {
"message": "资源管理"
},
"app.skins.add-button": {
"message": "添加皮肤"
},
"app.skins.add-button.drag-and-drop": {
"message": "拖放"
},
"app.skins.apply-button": {
"message": "应用"
},
"app.skins.delete-button": {
"message": "删除皮肤"
},
"app.skins.delete-modal.description": {
"message": "这会永久删除所选皮肤。这操作无法撤销。"
},
"app.skins.delete-modal.title": {
"message": "你确定要删除这个皮肤吗?"
},
"app.skins.dropped-file-error.text": {
"message": "无法读取拖放的文件。"
},
"app.skins.dropped-file-error.title": {
"message": "处理文件时出错"
},
"app.skins.edit-button": {
"message": "编辑皮肤"
},
"app.skins.modal.add-skin-button": {
"message": "添加皮肤"
},
"app.skins.modal.add-title": {
"message": "添加皮肤中"
},
"app.skins.modal.arm-style-section": {
"message": "手臂样式"
},
"app.skins.modal.arm-style-slim": {
"message": "细"
},
"app.skins.modal.arm-style-wide": {
"message": "粗"
},
"app.skins.modal.cape-fallback-name": {
"message": "披风"
},
"app.skins.modal.cape-section": {
"message": "披风"
},
"app.skins.modal.edit-title": {
"message": "编辑皮肤中"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "请先对皮肤进行编辑!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "无披风"
},
"app.skins.modal.none-cape-option": {
"message": "无"
},
"app.skins.modal.replace-texture-button": {
"message": "替换纹理"
},
"app.skins.modal.save-skin-button": {
"message": "保存皮肤"
},
"app.skins.modal.saving-tooltip": {
"message": "保存中……"
},
"app.skins.modal.texture-section": {
"message": "纹理"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "请先上传皮肤!"
},
"app.skins.preview.edit-button": {
"message": "编辑皮肤"
},
"app.skins.previewing-badge": {
"message": "预览"
},
"app.skins.rate-limit.text": {
"message": "你更改皮肤过于频繁。Mojang服务器已暂时阻止了后续请求。请稍等片刻后再试。"
},
"app.skins.rate-limit.title": {
"message": "慢点!"
},
"app.skins.section.builders-and-biomes": {
"message": "建造者与生物群系"
},
"app.skins.section.chase-the-skies": {
"message": "追逐苍穹"
},
"app.skins.section.default-skins": {
"message": "默认皮肤"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.mounts-of-mayhem": {
"message": "暴乱坐骑"
},
"app.skins.section.saved-skins": {
"message": "已保存皮肤"
},
"app.skins.section.striding-hero": {
"message": "阔步英雄"
},
"app.skins.section.the-copper-age": {
"message": "铜器时代"
},
"app.skins.section.the-garden-awakens": {
"message": "花园苏醒"
},
"app.skins.section.tiny-takeover": {
"message": "微小占领"
},
"app.skins.sign-in.button": {
"message": "登录"
},
"app.skins.sign-in.description": {
"message": "请登录您的 Minecraft 账号,以便使用 Modrinth 应用的皮肤管理功能。"
},
"app.skins.sign-in.rinthbot-alt": {
"message": "兴奋的 Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "请登录"
},
"app.skins.title": {
"message": "皮肤选择器"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} 更新已就绪!立即重启更新,或退出时自动安装。"
},
@@ -395,9 +518,6 @@
"friends.action.add-friend": {
"message": "添加好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 个好友请求"
},
"friends.add-friend.submit": {
"message": "发送好友请求"
},
@@ -567,7 +687,7 @@
"message": "启动 Hooks"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "自定义启动Hooks"
"message": "自定义启动 Hooks"
},
"instance.settings.tabs.hooks.description": {
"message": "Hooks允许高级用户在启动游戏前后运行特定的系统命令。"
@@ -611,6 +731,24 @@
"instance.settings.tabs.java": {
"message": "Java 及内存"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "自定义环境变量"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "自定义Java参数"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "自定义Java安装"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "自定义内存分配"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "输入环境变量……"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "输入 Java 参数……"
},
"instance.settings.tabs.java.environment-variables": {
"message": "环境变量"
},
@@ -626,6 +764,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "内存分配"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "游戏窗口"
},
+25 -19
View File
@@ -147,10 +147,10 @@
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "正在安裝模組包..."
"message": "正在安裝模組包……"
},
"app.export-modal.description-placeholder": {
"message": "輸入模組包描述..."
"message": "輸入模組包描述……"
},
"app.export-modal.export-button": {
"message": "匯出"
@@ -224,9 +224,6 @@
"app.instance.worlds.browse-servers": {
"message": "瀏覽伺服器"
},
"app.instance.worlds.delete-world-description": {
"message": "「{name}」將**永久刪除**,且無法還原。"
},
"app.instance.worlds.delete-world-title": {
"message": "確定要永久刪除這個世界嗎?"
},
@@ -248,17 +245,11 @@
"app.instance.worlds.no-worlds-heading": {
"message": "未新增任何伺服器或世界"
},
"app.instance.worlds.remove-server-description": {
"message": "「{name}」將從你的清單中移除(包含遊戲內),且無法還原。"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "「{name}」({address}) 將從你的清單中移除(包含遊戲內),且無法還原。"
},
"app.instance.worlds.remove-server-title": {
"message": "確定要移除「{name}」嗎?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "搜尋 {count} 個世界..."
"message": "搜尋 {count} 個世界……"
},
"app.instance.worlds.this-server": {
"message": "這個伺服器"
@@ -272,9 +263,6 @@
"app.modal.install-to-play.install-button": {
"message": "安裝"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} 個模組"
},
"app.modal.install-to-play.required-modpack": {
"message": "所需模組包"
},
@@ -303,7 +291,7 @@
"message": "這個專案已安裝"
},
"app.project.install-context.back-to-browse": {
"message": "回到瀏覽頁面"
"message": "返回瀏覽"
},
"app.project.install-context.install-content-to-instance": {
"message": "將內容安裝至實例"
@@ -395,9 +383,6 @@
"friends.action.add-friend": {
"message": "新增好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 個好友邀請"
},
"friends.add-friend.submit": {
"message": "送出好友邀請"
},
@@ -611,6 +596,24 @@
"instance.settings.tabs.java": {
"message": "Java 和記憶體"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "自訂環境變數"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "自訂 Java 參數"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "自訂 Java 安裝"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "自訂記憶體分配"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "輸入環境變數……"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "輸入 Java 參數……"
},
"instance.settings.tabs.java.environment-variables": {
"message": "環境變數"
},
@@ -626,6 +629,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "記憶體配置"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "視窗"
},
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
<script setup lang="ts">
import {
injectModrinthClient,
injectModrinthServerContext,
ServersManageAccessPage,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
const client = injectModrinthClient()
const { serverId } = injectModrinthServerContext()
const queryClient = useQueryClient()
try {
await Promise.all([
queryClient.ensureQueryData({
queryKey: ['servers', 'users', 'v1', serverId],
queryFn: () => client.archon.server_users_v1.list(serverId),
staleTime: 30_000,
}),
queryClient.ensureQueryData({
queryKey: ['servers', 'v1', 'detail', serverId],
queryFn: () => client.archon.servers_v1.get(serverId),
staleTime: 30_000,
}),
])
} catch {
// Let mounted layouts' useQuery surface errors; do not fail route setup.
}
function userProfileLink(username: string) {
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
}
</script>
<template>
<ServersManageAccessPage :user-profile-link="userProfileLink" />
</template>
@@ -1,7 +1,8 @@
import Access from './Access.vue'
import Backups from './Backups.vue'
import Content from './Content.vue'
import Files from './Files.vue'
import Index from './Index.vue'
import Overview from './Overview.vue'
export { Backups, Content, Files, Index, Overview }
export { Access, Backups, Content, Files, Index, Overview }
+56 -18
View File
@@ -32,14 +32,16 @@
{{ instance.loader }} {{ instance.game_version }}
</div>
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<template v-if="showInstancePlayTime">
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div class="flex items-center gap-2 font-medium">
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>
<template v-else> Never played </template>
</div>
<div class="flex items-center gap-2 font-medium">
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>
<template v-else> Never played </template>
</div>
</template>
</template>
<template v-else>
@@ -218,7 +220,11 @@
:key="instance.path"
>
<template v-if="Component">
<Suspense :key="instance.path">
<Suspense
:key="instance.path"
@pending="subpagePending = true"
@resolve="subpagePending = false"
>
<component
:is="Component"
:instance="instance"
@@ -228,6 +234,7 @@
:installed="instance.install_stage !== 'installed'"
:is-server-instance="isServerInstance"
:open-settings="() => settingsModal?.show(1)"
v-bind="contentSubpageProps"
@play="updatePlayState"
@stop="() => stopInstance('InstanceSubpage')"
></component>
@@ -295,6 +302,7 @@ import {
ServerPing,
ServerRecentPlays,
ServerRegion,
useLoadingBarToken,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
@@ -312,6 +320,7 @@ import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { trackEvent } from '@/helpers/analytics'
import { get_project_v3 } from '@/helpers/cache.js'
import { process_listener, profile_listener } from '@/helpers/events'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
@@ -319,7 +328,7 @@ import { showProfileInFolder } from '@/helpers/utils.js'
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
import { injectServerInstall } from '@/providers/server-install'
import { handleSevereError } from '@/store/error.js'
import { useBreadcrumbs } from '@/store/state'
import { useBreadcrumbs, useTheming } from '@/store/state'
dayjs.extend(duration)
dayjs.extend(relativeTime)
@@ -331,6 +340,9 @@ const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const themeStore = useTheming()
const showInstancePlayTime = computed(() => themeStore.getFeatureFlag('show_instance_play_time'))
const contentSubpageRouteNames = new Set(['Mods', 'ModsFilter'])
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -341,12 +353,16 @@ window.addEventListener('online', () => {
})
const instance = ref<GameInstance>()
const preloadedContent = ref<InstanceContentData | null>(null)
const playing = ref(false)
const loading = ref(false)
const subpagePending = ref(false)
const stopping = ref(false)
const exportModal = ref<InstanceType<typeof ExportModal>>()
const updateToPlayModal = ref<InstanceType<typeof UpdateToPlayModal>>()
useLoadingBarToken(subpagePending)
const isServerInstance = ref(false)
const linkedProjectV3 = ref<Labrinth.Projects.v3.Project>()
const selected = ref<unknown[]>([])
@@ -361,36 +377,55 @@ const playersOnline = ref<number | undefined>(undefined)
const ping = ref<number | undefined>(undefined)
const loadingServerPing = ref(false)
function isContentSubpageRoute(routeName = route.name) {
return typeof routeName === 'string' && contentSubpageRouteNames.has(routeName)
}
async function fetchInstance() {
isServerInstance.value = false
linkedProjectV3.value = undefined
preloadedContent.value = null
ping.value = undefined
playersOnline.value = undefined
loadingServerPing.value = false
instance.value = await get(route.params.id as string).catch(handleError)
const nextInstance = await get(route.params.id as string).catch(handleError)
let nextLinkedProjectV3: Labrinth.Projects.v3.Project | undefined
let nextIsServerInstance = false
if (!offline.value && instance.value?.linked_data && instance.value.linked_data.project_id) {
const contentPreloadPromise =
nextInstance && isContentSubpageRoute()
? loadInstanceContentData(nextInstance.path, undefined, handleError)
: Promise.resolve(null)
if (!offline.value && nextInstance?.linked_data && nextInstance.linked_data.project_id) {
try {
linkedProjectV3.value = await get_project_v3(
instance.value.linked_data.project_id,
nextLinkedProjectV3 = await get_project_v3(
nextInstance.linked_data.project_id,
'must_revalidate',
)
if (linkedProjectV3.value?.minecraft_server != null) {
isServerInstance.value = true
if (nextLinkedProjectV3?.minecraft_server != null) {
nextIsServerInstance = true
}
} catch (error) {
handleError(error as Error)
}
}
const nextPreloadedContent = await contentPreloadPromise
instance.value = nextInstance ?? undefined
linkedProjectV3.value = nextLinkedProjectV3
isServerInstance.value = nextIsServerInstance
preloadedContent.value = nextPreloadedContent
fetchDeferredData()
if (instance.value) {
if (nextInstance) {
queryClient.prefetchQuery({
queryKey: ['worlds', instance.value.path],
queryFn: () => refreshWorlds(instance.value!.path),
queryKey: ['worlds', nextInstance.path],
queryFn: () => refreshWorlds(nextInstance.path),
staleTime: 30_000,
})
}
@@ -448,6 +483,9 @@ const renderMode = computed<'scroll' | 'fixed'>(() =>
route.meta.renderMode === 'fixed' ? 'fixed' : 'scroll',
)
const isFixedRender = computed(() => renderMode.value === 'fixed')
const contentSubpageProps = computed(() =>
isContentSubpageRoute() ? { preloadedContent: preloadedContent.value } : {},
)
const tabs = computed(() => [
{
@@ -77,7 +77,7 @@ function buildLogList(rawLogs) {
log.filename !== 'latest_stdout.log' &&
log.filename !== 'latest_stdout' &&
log.filename !== 'launcher_log.txt' &&
log.stdout !== '' &&
(log.output == null || log.output !== '') &&
(log.filename.includes('.log') || log.filename.endsWith('.txt')),
)
.map((log) => ({
+462 -138
View File
@@ -13,6 +13,7 @@
:modpack-name="linkedModpackProject?.title"
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
:enable-toggle="!props.isServerInstance"
:busy="isBulkOperating"
:get-overflow-options="getOverflowOptions"
:switch-version="handleSwitchVersion"
@update:enabled="handleModpackContentToggle"
@@ -87,12 +88,12 @@ import {
ReadyTransition,
useDebugLogger,
useVIntl,
versionChangesGameVersion,
} from '@modrinth/ui'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { open } from '@tauri-apps/plugin-dialog'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useDebounceFn } from '@vueuse/core'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
@@ -100,22 +101,20 @@ import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import {
add_project_from_path,
add_project_from_version,
duplicate,
edit,
get,
get_content_items,
get_linked_modpack_content,
get_linked_modpack_info,
list,
remove_project,
toggle_disable_project,
update_managed_modrinth_version,
update_project,
} from '@/helpers/profile'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
import { highlightModInProfile } from '@/helpers/utils.js'
import { injectContentInstall } from '@/providers/content-install'
@@ -160,6 +159,7 @@ const props = defineProps<{
instance: GameInstance
isServerInstance?: boolean
openSettings?: () => void
preloadedContent?: InstanceContentData | null
}>()
const loading = ref(true)
@@ -212,6 +212,7 @@ const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
// TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow.
const updatingProject = ref<ContentItem | null>(null)
const updatingProjectVersions = ref<Labrinth.Versions.v2.Version[]>([])
const loadingVersions = ref(false)
@@ -219,6 +220,134 @@ const loadingChangelog = ref(false)
const updatingModpack = ref(false)
const pendingModpackUpdateVersion = ref<Labrinth.Versions.v2.Version | null>(null)
const isModpackUpdateDowngrade = ref(false)
const activeContentOperationKeys = ref(new Set<string>())
let activeContentOperationCount = 0
let updateRequestId = 0
const activeUpdateRequestId = ref(0)
function fileNameFromPath(path: string) {
return path.split('/').pop() ?? path
}
function getContentItemId(item: ContentItem | null | undefined) {
return item?.file_path ?? item?.file_name ?? item?.id ?? ''
}
function getContentOperationKeys(item: ContentItem) {
return [getContentItemId(item), item.file_path, item.file_name].filter(
(key): key is string => !!key,
)
}
function hasContentOperation(item: ContentItem) {
const keys = getContentOperationKeys(item)
return keys.some((key) => activeContentOperationKeys.value.has(key))
}
function setContentItemBusy(item: ContentItem, busy: boolean, originalFileName = item.file_name) {
item.installing = busy
modpackContentModal.value?.updateItem(originalFileName, {
installing: busy,
disabled: busy,
})
if (item.file_name !== originalFileName) {
modpackContentModal.value?.updateItem(item.file_name, {
installing: busy,
disabled: busy,
})
}
}
function beginContentOperation(item: ContentItem) {
if (hasContentOperation(item)) return null
const keys = getContentOperationKeys(item)
activeContentOperationKeys.value = new Set([...activeContentOperationKeys.value, ...keys])
activeContentOperationCount++
isBulkOperating.value = true
setContentItemBusy(item, true)
return { keys, originalFileName: item.file_name }
}
function finishContentOperation(
item: ContentItem,
operation: { keys: string[]; originalFileName: string },
) {
const nextKeys = new Set(activeContentOperationKeys.value)
for (const key of operation.keys) {
nextKeys.delete(key)
}
activeContentOperationKeys.value = nextKeys
activeContentOperationCount = Math.max(0, activeContentOperationCount - 1)
setContentItemBusy(item, false, operation.originalFileName)
if (activeContentOperationCount === 0) {
isBulkOperating.value = false
}
}
function beginUpdateRequest() {
updateRequestId++
activeUpdateRequestId.value = updateRequestId
return updateRequestId
}
function isActiveUpdateRequest(requestId: number) {
return activeUpdateRequestId.value === requestId
}
function sortVersionsByPublishedDate(versions: Labrinth.Versions.v2.Version[]) {
return [...versions].sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
}
function mergeVersionIntoList(
versions: Labrinth.Versions.v2.Version[],
version: Labrinth.Versions.v2.Version,
) {
const existingIndex = versions.findIndex((v) => v.id === version.id)
if (existingIndex === -1) {
return sortVersionsByPublishedDate([version, ...versions])
}
const mergedVersions = [...versions]
mergedVersions[existingIndex] = version
return sortVersionsByPublishedDate(mergedVersions)
}
async function getUpdaterProjectVersions(projectId: string, pinnedVersionId?: string) {
let fetchError: unknown = null
let versions = (await get_project_versions(projectId, 'bypass').catch((err) => {
fetchError = err
return null
})) as Labrinth.Versions.v2.Version[] | null
if (!versions) {
versions = (await get_project_versions(projectId).catch(() => null)) as
| Labrinth.Versions.v2.Version[]
| null
}
if (!versions && fetchError) {
handleError(fetchError as Error)
}
let mergedVersions = sortVersionsByPublishedDate(versions ?? [])
if (pinnedVersionId && !mergedVersions.some((version) => version.id === pinnedVersionId)) {
const pinnedVersion = (await get_version(pinnedVersionId, 'bypass').catch(
() => null,
)) as Labrinth.Versions.v2.Version | null
if (pinnedVersion) {
mergedVersions = mergeVersionIntoList(mergedVersions, pinnedVersion)
}
}
return mergedVersions
}
async function handleBrowseContent() {
if (!props.instance) return
@@ -265,9 +394,21 @@ async function handleUploadFiles() {
}
async function toggleDisableMod(mod: ContentItem) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
try {
mod.file_path = await toggle_disable_project(props.instance.path, mod.file_path!)
const newPath = await toggle_disable_project(props.instance.path, mod.file_path)
const newFileName = fileNameFromPath(newPath)
mod.file_path = newPath
mod.file_name = newFileName
mod.enabled = !mod.enabled
modpackContentModal.value?.updateItem(operation.originalFileName, {
file_path: newPath,
file_name: newFileName,
enabled: mod.enabled,
})
trackEvent('InstanceProjectDisable', {
loader: props.instance.loader,
@@ -279,33 +420,48 @@ async function toggleDisableMod(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(mod, operation)
}
}
const toggleDisableDebounced = useDebounceFn(toggleDisableMod, 20)
const toggleDisableDebounced = toggleDisableMod
async function removeMod(mod: ContentItem) {
await remove_project(props.instance.path, mod.file_path!).catch(handleError)
projects.value = projects.value.filter((x) => mod.file_path !== x.file_path)
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
try {
const removedPath = mod.file_path
await remove_project(props.instance.path, removedPath)
projects.value = projects.value.filter((x) => removedPath !== x.file_path)
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(mod, operation)
}
}
async function updateProject(mod: ContentItem) {
try {
const newPath = await update_project(props.instance.path, mod.file_path!)
mod.file_path = newPath
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
if (mod.update_version_id) {
const versionData = await get_version(mod.update_version_id, 'must_revalidate').catch(
handleError,
)
try {
const updateVersionId = mod.update_version_id
await update_project(props.instance.path, mod.file_path)
if (updateVersionId) {
const versionData = await get_version(updateVersionId, 'must_revalidate').catch(handleError)
if (versionData) {
const profile = await get(props.instance.path).catch(handleError)
@@ -316,12 +472,6 @@ async function updateProject(mod: ContentItem) {
}
}
mod.has_update = false
if (mod.version && mod.update_version_id) {
mod.version.id = mod.update_version_id
}
mod.update_version_id = null
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
@@ -331,39 +481,65 @@ async function updateProject(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
} finally {
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
}
}
async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions.v2.Version) {
isBulkOperating.value = true
mod.installing = true
if (mod.version) {
mod.version.id = version.id
mod.version.version_number = version.version_number
}
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
const oldPath = mod.file_path
const wasDisabled = mod.enabled === false || oldPath.endsWith('.disabled')
let newPath: string | null = null
let shouldRemoveNewOnError = false
try {
await remove_project(props.instance.path, mod.file_path!)
const newPath = await add_project_from_version(props.instance.path, version.id, 'standalone')
newPath = await add_project_from_version(props.instance.path, version.id, 'update')
shouldRemoveNewOnError = newPath !== oldPath
if (wasDisabled) {
newPath = await toggle_disable_project(props.instance.path, newPath)
}
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, version, 'update').catch(handleError)
}
mod.file_path = newPath
shouldRemoveNewOnError = false
if (newPath !== oldPath) {
await remove_project(props.instance.path, oldPath)
}
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
} catch (err) {
if (shouldRemoveNewOnError && newPath && newPath !== oldPath) {
await remove_project(props.instance.path, newPath).catch(() => {})
}
handleError(err as Error)
} finally {
mod.installing = false
isBulkOperating.value = false
await initProjects()
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
}
}
async function handleUpdate(id: string) {
const item = projects.value.find((p) => p.id === id)
const item = projects.value.find((p) => getContentItemId(p) === id)
if (!item?.has_update || !item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
debug('handleUpdate triggered', {
fileName: item.file_name,
projectType: item.project_type,
@@ -384,15 +560,49 @@ async function handleUpdate(id: string) {
await nextTick()
contentUpdaterModal.value?.show(item.update_version_id ?? undefined)
const initialVersionId = item.update_version_id ?? undefined
debug('handleUpdate: opening content updater modal', {
type: 'content',
initialVersionId,
item: {
id: item.id,
fileName: item.file_name,
projectType: item.project_type,
projectId: item.project.id,
projectTitle: item.project.title,
currentVersionId: item.version.id,
currentVersionNumber: item.version.version_number,
updateVersionId: item.update_version_id,
},
instance: {
path: props.instance.path,
name: props.instance.name,
gameVersion: props.instance.game_version,
loader: props.instance.loader,
linkedData: props.instance.linked_data,
},
modalStateBeforeFetch: {
updatingModpack: updatingModpack.value,
updatingProjectId: updatingProject.value?.id,
updatingProjectVersions: updatingProjectVersions.value.map((version) => ({
id: version.id,
versionNumber: version.version_number,
gameVersions: version.game_versions,
loaders: version.loaders,
datePublished: version.date_published,
})),
},
})
contentUpdaterModal.value?.show(initialVersionId)
const versions = (await get_project_versions(item.project.id).catch((e) => {
return handleError(e)
})) as Labrinth.Versions.v2.Version[] | null
const versions = await getUpdaterProjectVersions(item.project.id, initialVersionId)
if (!isActiveUpdateRequest(requestId) || getContentItemId(updatingProject.value) !== itemId)
return
loadingVersions.value = false
if (!versions) {
if (versions.length === 0) {
debug('handleUpdate: no versions returned', { projectId: item.project.id })
return
}
@@ -411,9 +621,25 @@ async function handleUpdate(id: string) {
updateVersionInList: versions.some((v) => v.id === item.update_version_id),
})
versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
const preselectedVersion =
versions.find((version) => version.id === initialVersionId) ?? versions[0] ?? null
debug('handleUpdate: resolved content updater preselection', {
type: 'content',
initialVersionId,
foundInitialVersion: versions.some((version) => version.id === initialVersionId),
preselectedVersion: preselectedVersion
? {
id: preselectedVersion.id,
versionNumber: preselectedVersion.version_number,
gameVersions: preselectedVersion.game_versions,
loaders: preselectedVersion.loaders,
datePublished: preselectedVersion.date_published,
}
: null,
versionCount: versions.length,
currentVersionId: item.version.id,
updateVersionId: item.update_version_id,
})
updatingProjectVersions.value = versions
}
@@ -421,6 +647,9 @@ async function handleUpdate(id: string) {
async function handleSwitchVersion(item: ContentItem) {
if (!item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
updatingModpack.value = false
updatingProject.value = item
updatingProjectVersions.value = []
@@ -429,20 +658,16 @@ async function handleSwitchVersion(item: ContentItem) {
await nextTick()
contentUpdaterModal.value?.show(item.version.id, { switchMode: true })
const initialVersionId = item.version.id
contentUpdaterModal.value?.show(initialVersionId, { switchMode: true })
const versions = (await get_project_versions(item.project.id).catch((e) => {
return handleError(e)
})) as Labrinth.Versions.v2.Version[] | null
const versions = await getUpdaterProjectVersions(item.project.id, initialVersionId)
if (!isActiveUpdateRequest(requestId) || getContentItemId(updatingProject.value) !== itemId)
return
loadingVersions.value = false
if (!versions) return
versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
updatingProjectVersions.value = versions
}
@@ -468,9 +693,28 @@ async function handleModpackContent() {
}
}
async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
if (!props.instance?.path) return
const contentItems = await get_linked_modpack_content(props.instance.path, cacheBehaviour).catch(
handleError,
)
if (contentItems) {
modpackContentModal.value?.setItems(contentItems)
}
}
async function refreshContentState(cacheBehaviour?: CacheBehaviour) {
await initProjects(cacheBehaviour)
await refreshModpackContentItems(cacheBehaviour)
}
async function handleModpackUpdate() {
if (!props.instance?.linked_data?.project_id) return
const requestId = beginUpdateRequest()
updatingModpack.value = true
updatingProject.value = null
updatingProjectVersions.value = []
@@ -479,21 +723,66 @@ async function handleModpackUpdate() {
await nextTick()
contentUpdaterModal.value?.show(
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined,
const initialVersionId =
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined
debug('handleModpackUpdate: opening modpack updater modal', {
type: 'modpack',
initialVersionId,
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
linkedModpackProject: linkedModpackProject.value,
linkedModpackVersion: linkedModpackVersion.value,
linkedModpackHasUpdate: linkedModpackHasUpdate.value,
instance: {
path: props.instance.path,
name: props.instance.name,
gameVersion: props.instance.game_version,
loader: props.instance.loader,
linkedData: props.instance.linked_data,
},
modalStateBeforeFetch: {
updatingModpack: updatingModpack.value,
updatingProjectId: updatingProject.value?.id,
updatingProjectVersions: updatingProjectVersions.value.map((version) => ({
id: version.id,
versionNumber: version.version_number,
gameVersions: version.game_versions,
loaders: version.loaders,
datePublished: version.date_published,
})),
},
})
contentUpdaterModal.value?.show(initialVersionId)
const versions = await getUpdaterProjectVersions(
props.instance.linked_data.project_id,
initialVersionId,
)
const versions = (await get_project_versions(props.instance.linked_data.project_id).catch(
handleError,
)) as Labrinth.Versions.v2.Version[] | null
if (!isActiveUpdateRequest(requestId) || !updatingModpack.value) return
loadingVersions.value = false
if (!versions) return
if (versions.length === 0) return
versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
const preselectedVersion =
versions.find((version) => version.id === initialVersionId) ?? versions[0] ?? null
debug('handleModpackUpdate: resolved modpack updater preselection', {
type: 'modpack',
initialVersionId,
foundInitialVersion: versions.some((version) => version.id === initialVersionId),
preselectedVersion: preselectedVersion
? {
id: preselectedVersion.id,
versionNumber: preselectedVersion.version_number,
gameVersions: preselectedVersion.game_versions,
loaders: preselectedVersion.loaders,
datePublished: preselectedVersion.date_published,
}
: null,
versionCount: versions.length,
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
currentLinkedVersionId: props.instance.linked_data.version_id,
})
updatingProjectVersions.value = versions
}
@@ -502,32 +791,38 @@ async function fetchAndSpliceVersion(
versionId: string,
cacheBehaviour?: Parameters<typeof get_version>[1],
onError?: (err: unknown) => void,
requestId = activeUpdateRequestId.value,
) {
const fullVersion = (await get_version(versionId, cacheBehaviour).catch(
onError ?? (() => null),
)) as Labrinth.Versions.v2.Version | null
if (!isActiveUpdateRequest(requestId)) return
if (!fullVersion) return
const index = updatingProjectVersions.value.findIndex((v) => v.id === versionId)
if (index !== -1) {
const newVersions = [...updatingProjectVersions.value]
newVersions[index] = fullVersion
updatingProjectVersions.value = newVersions
}
updatingProjectVersions.value = mergeVersionIntoList(updatingProjectVersions.value, fullVersion)
}
async function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
if (version.changelog != null) return
const requestId = activeUpdateRequestId.value
loadingChangelog.value = true
await fetchAndSpliceVersion(version.id, 'must_revalidate', handleError as (err: unknown) => void)
loadingChangelog.value = false
await fetchAndSpliceVersion(
version.id,
'must_revalidate',
handleError as (err: unknown) => void,
requestId,
)
if (isActiveUpdateRequest(requestId)) {
loadingChangelog.value = false
}
}
async function handleVersionHover(version: Labrinth.Versions.v2.Version) {
if (version.changelog != null) return
await fetchAndSpliceVersion(version.id)
await fetchAndSpliceVersion(version.id, undefined, undefined, activeUpdateRequestId.value)
}
function resetUpdateState() {
activeUpdateRequestId.value = 0
updatingModpack.value = false
updatingProject.value = null
updatingProjectVersions.value = []
@@ -535,13 +830,23 @@ function resetUpdateState() {
loadingChangelog.value = false
}
function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
pendingModpackUpdateVersion.value = selectedVersion
const currentVersionId = props.instance?.linked_data?.version_id
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
isModpackUpdateDowngrade.value = currentVersion
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
: false
const shouldShowWarning =
isModpackUpdateDowngrade.value ||
versionChangesGameVersion(selectedVersion, props.instance.game_version)
if (!shouldShowWarning) {
await handleModpackUpdateConfirm()
return
}
modpackUpdateConfirmModal.value?.show()
}
@@ -551,6 +856,7 @@ async function handleModpackUpdateConfirm() {
const version = pendingModpackUpdateVersion.value
pendingModpackUpdateVersion.value = null
contentUpdaterModal.value?.hide()
isModpackUpdating.value = true
try {
await update_managed_modrinth_version(props.instance.path, version.id)
@@ -574,7 +880,7 @@ async function handleModalUpdate(
pendingModpackUpdateVersion.value = selectedVersion
await handleModpackUpdateConfirm()
} else {
handleModpackUpdateRequest(selectedVersion)
await handleModpackUpdateRequest(selectedVersion)
}
} else if (updatingProject.value) {
const mod = updatingProject.value
@@ -662,51 +968,31 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
async function initProjects(cacheBehaviour?: CacheBehaviour) {
if (!props.instance) return
const [contentItems, modpackInfo, allCategories] = await Promise.all([
get_content_items(props.instance.path, cacheBehaviour).catch(handleError),
get_linked_modpack_info(props.instance.path, cacheBehaviour).catch(handleError),
get_categories().catch(handleError),
])
const contentData = await loadInstanceContentData(
props.instance.path,
cacheBehaviour,
handleError,
)
applyContentData(contentData)
}
if (!contentItems) {
function applyContentData(contentData: InstanceContentData) {
if (contentData.path !== props.instance.path) return false
if (!contentData.contentItems) {
loading.value = false
return
return true
}
projects.value = contentItems
projects.value = contentData.contentItems
if (modpackInfo) {
linkedModpackProject.value = {
...modpackInfo.project,
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
icon_url: modpackInfo.project.icon_url ?? undefined,
}
linkedModpackVersion.value = {
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
}
linkedModpackOwner.value = modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null
linkedModpackHasUpdate.value = modpackInfo.has_update
linkedModpackUpdateVersionId.value = modpackInfo.update_version_id
if (allCategories && modpackInfo.project.categories) {
const seen = new Set<string>()
linkedModpackCategories.value = allCategories.filter((cat: { name: string }) => {
if (modpackInfo.project.categories.includes(cat.name) && !seen.has(cat.name)) {
seen.add(cat.name)
return true
}
return false
})
} else {
linkedModpackCategories.value = []
}
if (contentData.modpack) {
linkedModpackProject.value = contentData.modpack.project
linkedModpackVersion.value = contentData.modpack.version
linkedModpackOwner.value = contentData.modpack.owner
linkedModpackCategories.value = contentData.modpack.categories
linkedModpackHasUpdate.value = contentData.modpack.hasUpdate
linkedModpackUpdateVersionId.value = contentData.modpack.updateVersionId
} else {
linkedModpackProject.value = null
linkedModpackVersion.value = null
@@ -717,6 +1003,7 @@ async function initProjects(cacheBehaviour?: CacheBehaviour) {
}
loading.value = false
return true
}
provideAppBackup({
@@ -809,8 +1096,9 @@ provideContentManager({
showContentHint,
dismissContentHint,
shareItems: handleShareItems,
getItemId: getContentItemId,
mapToTableItem: (item: ContentItem) => ({
id: item.id,
id: getContentItemId(item),
project: item.project ?? {
id: item.file_name,
slug: null,
@@ -844,10 +1132,22 @@ provideContentManager({
filterPersistKey: props.instance.path,
})
await initProjects()
type UnlistenFn = () => void
const initialContentReady = loadInitialContent()
void initialContentReady.then(restoreModpackContentModalState).catch(handleError)
function loadInitialContent() {
if (props.preloadedContent && applyContentData(props.preloadedContent)) {
return Promise.resolve()
}
return initProjects()
}
async function restoreModpackContentModalState() {
if (!savedModalState) return
// Restore modpack content modal state if returning via back navigation
if (savedModalState) {
const stateToRestore = savedModalState
savedModalState = null
await nextTick()
@@ -860,18 +1160,32 @@ const removeBeforeEach = router.beforeEach(() => {
savedModalState = state ?? null
})
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
let isUnmounted = false
let unlistenDragDrop: UnlistenFn | null = null
let unlistenProfiles: UnlistenFn | null = null
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError)
}
await initProjects()
})
onMounted(() => {
void getCurrentWebview()
.onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
const unlistenProfiles = await profile_listener(
async (event: { event: string; profile_path_id: string }) => {
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError)
}
await initProjects()
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenDragDrop = unlisten
})
.catch(handleError)
void profile_listener(async (event: { event: string; profile_path_id: string }) => {
if (
props.instance &&
event.profile_path_id === props.instance.path &&
@@ -881,8 +1195,17 @@ const unlistenProfiles = await profile_listener(
) {
await initProjects()
}
},
)
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenProfiles = unlisten
})
.catch(handleError)
})
watch(
() => props.instance?.install_stage,
@@ -905,8 +1228,9 @@ watch(
)
onUnmounted(() => {
isUnmounted = true
removeBeforeEach()
unlisten()
unlistenProfiles()
unlistenDragDrop?.()
unlistenProfiles?.()
})
</script>
@@ -1,6 +1,6 @@
import type { Labrinth } from '@modrinth/api-client'
import type { ContentInstallInstance, ContentInstallProjectInfo, ContentItem } from '@modrinth/ui'
import { createContext } from '@modrinth/ui'
import { createContext, defineMessage, useVIntl } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import dayjs from 'dayjs'
@@ -34,7 +34,7 @@ import {
} from '@/store/install.js'
interface ModalRef {
show: () => void
show: (initialVersionId?: string) => void
hide: () => void
}
@@ -42,19 +42,14 @@ interface ModpackAlreadyInstalledModalRef {
show: (instanceName: string, instancePath: string) => void
}
interface IncompatibilityWarningModalRef {
show: (
instance: GameInstance,
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
version: Labrinth.Versions.v2.Version,
callback: (versionId?: string) => void,
) => void
}
const LOADER_ORDER = ['vanilla', 'fabric', 'quilt', 'neoforge', 'forge']
const SUPPORTED_LOADERS: Set<string> = new Set(['vanilla', 'forge', 'fabric', 'quilt', 'neoforge'])
const VANILLA_COMPATIBLE_LOADERS: Set<string> = new Set(['minecraft', 'datapack'])
const noCompatibleVersionsMessage = defineMessage({
id: 'app.content-install.no-compatible-versions',
defaultMessage:
'No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically.',
})
function sortLoaders(loaders: string[]): string[] {
return loaders.slice().sort((a, b) => {
@@ -91,7 +86,17 @@ export interface ContentInstallContext {
setModpackAlreadyInstalledModal: (ref: ModpackAlreadyInstalledModalRef) => void
handleModpackDuplicateCreateAnyway: () => Promise<void>
handleModpackDuplicateGoToInstance: (instancePath: string) => void
setIncompatibilityWarningModal: (ref: IncompatibilityWarningModalRef) => void
setIncompatibilityWarningModal: (ref: ModalRef) => void
incompatibilityWarningVersions: Ref<Labrinth.Versions.v2.Version[]>
incompatibilityWarningCurrentGameVersion: Ref<string>
incompatibilityWarningCurrentLoader: Ref<string>
incompatibilityWarningProjectType: Ref<string | undefined>
incompatibilityWarningProjectIconUrl: Ref<string | undefined>
incompatibilityWarningProjectName: Ref<string | undefined>
incompatibilityWarningMessage: Ref<string | undefined>
incompatibilityWarningInstalling: Ref<boolean>
handleIncompatibilityWarningInstall: (version: Labrinth.Versions.v2.Version) => Promise<void>
handleIncompatibilityWarningCancel: () => void
install: (
projectId: string,
versionId?: string | null,
@@ -113,6 +118,7 @@ export function createContentInstall(opts: {
router: Router
handleError: (err: unknown) => void
}): ContentInstallContext {
const { formatMessage } = useVIntl()
const instances = ref<ContentInstallInstance[]>([])
const compatibleLoaders = ref<string[]>([])
const gameVersions = ref<string[]>([])
@@ -124,6 +130,14 @@ export function createContentInstall(opts: {
const projectInfo = ref<ContentInstallProjectInfo | null>(null)
const installingItems = ref<Map<string, ContentItem[]>>(new Map())
const incompatibilityWarningVersions = ref<Labrinth.Versions.v2.Version[]>([])
const incompatibilityWarningCurrentGameVersion = ref('')
const incompatibilityWarningCurrentLoader = ref('')
const incompatibilityWarningProjectType = ref<string | undefined>(undefined)
const incompatibilityWarningProjectIconUrl = ref<string | undefined>(undefined)
const incompatibilityWarningProjectName = ref<string | undefined>(undefined)
const incompatibilityWarningMessage = ref<string | undefined>(undefined)
const incompatibilityWarningInstalling = ref(false)
function addInstallingItem(
instancePath: string,
@@ -239,11 +253,15 @@ export function createContentInstall(opts: {
let modalRef: ModalRef | null = null
let modpackAlreadyInstalledModalRef: ModpackAlreadyInstalledModalRef | null = null
let incompatibilityWarningModalRef: IncompatibilityWarningModalRef | null = null
let incompatibilityWarningModalRef: ModalRef | null = null
let currentProject: Labrinth.Projects.v2.Project | null = null
let currentVersions: Labrinth.Versions.v2.Version[] = []
let currentCallback: (versionId?: string) => void = () => {}
let profileMap: Record<string, GameInstance> = {}
let incompatibilityWarningInstance: GameInstance | null = null
let incompatibilityWarningProject: Labrinth.Projects.v2.Project | null = null
let incompatibilityWarningCallback: (versionId?: string) => void = () => {}
let incompatibilityWarningInstalled = false
let pendingModpackInstall: {
project: Labrinth.Projects.v2.Project
@@ -410,15 +428,35 @@ export function createContentInstall(opts: {
async function handleInstallToInstance(instance: ContentInstallInstance) {
const profile = profileMap[instance.id]
const storeInstance = instances.value.find((i) => i.id === instance.id)
if (storeInstance) storeInstance.installing = true
if (!currentProject || !profile) {
opts.handleError('No project or instance found')
return
}
const version = findPreferredVersion(currentVersions, currentProject, profile)
if (!version) {
if (storeInstance) storeInstance.installing = false
opts.handleError('No compatible version found')
if (currentVersions.length > 0 && incompatibilityWarningModalRef) {
const onIncompatibleInstall = (versionId?: string) => {
if (versionId && storeInstance) {
storeInstance.installed = true
}
currentCallback(versionId)
}
await showIncompatibilityWarning(
profile,
currentProject,
currentVersions,
currentVersions[0],
onIncompatibleInstall,
)
} else {
opts.handleError('No version found')
}
return
}
if (storeInstance) storeInstance.installing = true
const installedProjectIds: string[] = []
if (currentProject) {
addInstallingItem(instance.id, currentProject, version)
@@ -458,6 +496,73 @@ export function createContentInstall(opts: {
}
}
async function showIncompatibilityWarning(
instance: GameInstance,
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
version: Labrinth.Versions.v2.Version,
callback: (versionId?: string) => void,
) {
incompatibilityWarningInstance = instance
incompatibilityWarningProject = project
incompatibilityWarningCallback = callback
incompatibilityWarningInstalled = false
incompatibilityWarningInstalling.value = false
incompatibilityWarningVersions.value = versions
incompatibilityWarningCurrentGameVersion.value = instance.game_version ?? ''
incompatibilityWarningCurrentLoader.value = instance.loader ?? ''
incompatibilityWarningProjectType.value = project.project_type
incompatibilityWarningProjectIconUrl.value = project.icon_url ?? undefined
incompatibilityWarningProjectName.value = project.title
const compatibilityLabel =
project.project_type === 'resourcepack' || project.project_type === 'datapack'
? (instance.game_version ?? '')
: `${instance.loader ?? ''} ${instance.game_version ?? ''}`.trim()
incompatibilityWarningMessage.value = formatMessage(noCompatibleVersionsMessage, {
compatibilityLabel,
})
await nextTick()
incompatibilityWarningModalRef?.show(version.id)
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
}
async function handleIncompatibilityWarningInstall(version: Labrinth.Versions.v2.Version) {
if (!incompatibilityWarningInstance || !incompatibilityWarningProject) return
incompatibilityWarningInstalling.value = true
try {
await add_project_from_version(incompatibilityWarningInstance.path, version.id, 'standalone')
} catch (err) {
opts.handleError(err)
incompatibilityWarningInstalling.value = false
return
}
incompatibilityWarningInstalling.value = false
incompatibilityWarningInstalled = true
incompatibilityWarningCallback(version.id)
incompatibilityWarningModalRef?.hide()
trackEvent('ProjectInstall', {
loader: incompatibilityWarningInstance.loader,
game_version: incompatibilityWarningInstance.game_version,
id: incompatibilityWarningProject.id,
version_id: version.id,
project_type: incompatibilityWarningProject.project_type,
title: incompatibilityWarningProject.title,
source: 'ProjectIncompatibilityWarningModal',
})
}
function handleIncompatibilityWarningCancel() {
if (!incompatibilityWarningInstalled) {
incompatibilityWarningCallback()
}
incompatibilityWarningInstalled = false
}
async function handleCreateAndInstall(data: {
name: string
iconPath: string | null
@@ -614,7 +719,7 @@ export function createContentInstall(opts: {
removeInstallingItems(instancePath, installedProjectIds)
}
} else {
incompatibilityWarningModalRef?.show(instance, project, projectVersions, version, callback)
await showIncompatibilityWarning(instance, project, projectVersions, version, callback)
}
} else {
let versions = (
@@ -668,9 +773,19 @@ export function createContentInstall(opts: {
pendingModpackInstall = null
opts.router.push(`/instance/${encodeURIComponent(instancePath)}`)
},
setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) {
setIncompatibilityWarningModal(ref: ModalRef) {
incompatibilityWarningModalRef = ref
},
incompatibilityWarningVersions,
incompatibilityWarningCurrentGameVersion,
incompatibilityWarningCurrentLoader,
incompatibilityWarningProjectType,
incompatibilityWarningProjectIconUrl,
incompatibilityWarningProjectName,
incompatibilityWarningMessage,
incompatibilityWarningInstalling,
handleIncompatibilityWarningInstall,
handleIncompatibilityWarningCancel,
install,
installingItems,
}
@@ -8,6 +8,7 @@ export interface InstanceSettingsContext {
offline?: boolean
isMinecraftServer: Ref<boolean>
onUnlinked: () => void
closeModal?: () => void
}
export const [injectInstanceSettings, provideInstanceSettings] =
@@ -1,5 +1,5 @@
import type { Labrinth } from '@modrinth/api-client'
import { type AuthProvider, provideAuth } from '@modrinth/ui'
import { type AuthProvider, type AuthUser, provideAuth } from '@modrinth/ui'
import { computed, type Ref, ref, watchEffect } from 'vue'
type AppCredentials = {
@@ -12,7 +12,7 @@ export function setupAuthProvider(
requestSignIn: (redirectPath: string) => void | Promise<void>,
) {
const sessionToken = ref<string | null>(null)
const user = ref<Labrinth.Users.v2.User | null>(null)
const user = ref<AuthUser | null>(null)
const isReady = computed(() => credentials.value !== undefined)
const authProvider: AuthProvider = {
@@ -26,7 +26,7 @@ export function setupFilePickerProvider() {
const file = await createFileFromPath(path, 'icon')
return { file, path, previewUrl: convertFileSrc(path) }
},
async pickModpackFile() {
async pickModpackFile(options) {
const result = await open({
multiple: false,
filters: [{ name: 'Modpack', extensions: ['mrpack'] }],
@@ -34,12 +34,19 @@ export function setupFilePickerProvider() {
if (!result) return null
const path = result.path ?? result
if (!path) return null
const file = await createFileFromPath(
if (options?.readFile === false) {
// Instance imports stream from the native path, keeping large packs out of JS memory.
return { path, previewUrl: '' }
}
return {
file: await createFileFromPath(
path,
'modpack.mrpack',
'application/x-modrinth-modpack+zip',
),
path,
'modpack.mrpack',
'application/x-modrinth-modpack+zip',
)
return { file, path, previewUrl: '' }
previewUrl: '',
}
},
})
}
@@ -17,6 +17,7 @@ import {
writePendingServerContentInstallBaseline,
writeStoredServerInstallQueue,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
@@ -208,6 +209,7 @@ export function createServerInstallContent(opts: {
const router = useRouter()
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const queryClient = useQueryClient()
const serverIdQuery = computed(() => readQueryString(route.query.sid))
const worldIdQuery = computed(() => readQueryString(route.query.wid))
@@ -471,6 +473,9 @@ export function createServerInstallContent(opts: {
...serverContentInstallKeys.value,
...result.flushedPlans.map((plan) => plan.projectId),
])
if (result.flushedPlans.length > 0) {
await queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] })
}
return true
} finally {
@@ -491,6 +496,7 @@ export function createServerInstallContent(opts: {
const plans = new Map(queuedServerInstalls.value)
if (sid && wid) {
writeStoredServerInstallQueue(sid, wid, plans)
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
void getQueuedInstallPlaceholders(client, plans)
+10 -2
View File
@@ -73,6 +73,14 @@ export default new createRouter({
breadcrumb: [{ name: '?Server' }],
},
},
{
path: 'access',
name: 'ServerManageAccess',
component: Hosting.Access,
meta: {
breadcrumb: [{ name: '?Server' }],
},
},
],
},
{
@@ -86,10 +94,10 @@ export default new createRouter({
},
{
path: '/skins',
name: 'Skins',
name: 'Skin selector',
component: Pages.Skins,
meta: {
breadcrumb: [{ name: 'Skins' }],
breadcrumb: [{ name: 'Skin selector' }],
},
},
{
+4
View File
@@ -9,7 +9,9 @@ export const DEFAULT_FEATURE_FLAGS = {
server_ram_as_bytes_always_on: false,
always_show_app_controls: false,
skip_unknown_pack_warning: false,
pride_fundraiser: true,
i18n_debug: false,
show_instance_play_time: true,
}
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
@@ -21,6 +23,7 @@ export type ColorTheme = (typeof THEME_OPTIONS)[number]
export type ThemeStore = {
selectedTheme: ColorTheme
advancedRendering: boolean
hideNametagSkinsPage: boolean
toggleSidebar: boolean
devMode: boolean
@@ -30,6 +33,7 @@ export type ThemeStore = {
export const DEFAULT_THEME_STORE: ThemeStore = {
selectedTheme: 'dark',
advancedRendering: true,
hideNametagSkinsPage: false,
toggleSidebar: false,
devMode: false,
+1
View File
@@ -19,6 +19,7 @@
"types": ["vite/client"],
"paths": {
"@modrinth/api-client": ["../../packages/api-client/src/index.ts"],
"@/*": ["./src/*"]
}
},
+8
View File
@@ -8,6 +8,7 @@ import tauriConf from '../app/tauri.conf.json'
const projectRootDir = resolve(__dirname)
const appLibEnvDir = resolve(projectRootDir, '../../packages/app-lib')
const apiClientSource = resolve(projectRootDir, '../../packages/api-client/src/index.ts')
// Load .env from app-lib manually instead of using Vite's envDir, which would auto-load .env.local and override values
const envFilePath = resolve(appLibEnvDir, '.env')
@@ -37,6 +38,10 @@ export default defineConfig({
},
resolve: {
alias: [
{
find: '@modrinth/api-client',
replacement: apiClientSource,
},
{
find: '@',
replacement: resolve(projectRootDir, 'src'),
@@ -53,6 +58,9 @@ export default defineConfig({
params: {
overrides: {
removeViewBox: false,
cleanupIds: {
minify: false,
},
},
},
},
+13
View File
@@ -51,8 +51,21 @@ tauri-build = { workspace = true, features = ["codegen"] }
[target.'cfg(target_os = "linux")'.dependencies]
tauri-plugin-updater = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-graphics.workspace = true
objc2-app-kit = { workspace = true, features = [
"NSWindow",
] }
[target.'cfg(windows)'.dependencies]
webview2-com.workspace = true
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_UI_WindowsAndMessaging",
] }
windows-core.workspace = true
[features]
+3 -1
View File
@@ -114,10 +114,12 @@ fn main() {
"get_available_capes",
"get_available_skins",
"add_and_equip_custom_skin",
"set_default_cape",
"equip_skin",
"remove_custom_skin",
"save_custom_skin",
"unequip_skin",
"flush_pending_skin_change",
"flush_pending_skin_change_for_profile",
"normalize_skin_texture",
"get_dragged_skin_data",
])
+6
View File
@@ -42,6 +42,12 @@
{
"url": "https://api.purpurmc.org/*"
},
{
"url": "http://localhost:8000/*"
},
{
"url": "http://127.0.0.1:8000/*"
},
{
"url": "http://*.taila228c5.ts.net/*"
},
+118 -16
View File
@@ -11,11 +11,14 @@ use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub modal_shown: bool,
pub occluded: bool,
pub last_click: Option<Instant>,
pub malicious_origins: HashSet<String>,
}
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
#[cfg(any(windows, target_os = "macos"))]
pub(super) const OCCLUDED_AREA_THRESHOLD: f64 = 0.5;
#[cfg(not(target_os = "linux"))]
const ADS_USER_AGENT: &str = concat!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ",
@@ -60,8 +63,8 @@ fn configure_ads_cookie_settings(
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
) {
use webview2_com::Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
ICoreWebView2_13, ICoreWebView2Profile3,
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2_13,
ICoreWebView2Profile3,
};
use windows_core::Interface;
@@ -91,10 +94,12 @@ fn configure_ads_cookie_settings(
}
}
fn set_webview_visible<R: Runtime>(
webview: &tauri::Webview<R>,
_visible: bool,
) {
fn set_webview_visible<R: Runtime>(webview: &tauri::Webview<R>, visible: bool) {
#[cfg(not(any(windows, target_os = "macos")))]
{
_ = visible;
}
webview
.with_webview(
#[allow(unused_variables)]
@@ -102,11 +107,18 @@ fn set_webview_visible<R: Runtime>(
#[cfg(windows)]
{
let controller = wv.controller();
unsafe { controller.SetIsVisible(_visible) }.ok();
unsafe { controller.SetIsVisible(visible) }.ok();
}
},
)
.ok();
#[cfg(target_os = "macos")]
if visible {
webview.show().ok();
} else {
webview.hide().ok();
}
}
fn set_webview_visible_for_window<R: Runtime>(
@@ -119,7 +131,76 @@ fn set_webview_visible_for_window<R: Runtime>(
.and_then(|window| window.is_minimized().ok())
.unwrap_or(false);
set_webview_visible(webview, visible && !is_minimized);
let is_occluded = app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| state.occluded)
.unwrap_or(false);
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
}
#[cfg(any(windows, target_os = "macos"))]
fn compute_ads_webview_occlusion<R: Runtime>(
app: &tauri::AppHandle<R>,
) -> Option<bool> {
let main_window = app.get_window("main")?;
let webviews = app.webviews();
let webview = webviews.get("ads-window")?;
#[cfg(target_os = "macos")]
{
let position = webview.position().ok()?;
let size = webview.size().ok()?;
Some(
crate::api::ads_occlusion_macos::is_ads_webview_occluded(
&main_window,
position.x,
position.y,
size.width,
size.height,
)
.unwrap_or(false),
)
}
#[cfg(windows)]
{
let position = webview.position().ok()?;
let size = webview.size().ok()?;
let hwnd = main_window.hwnd().ok()?;
Some(crate::api::ads_occlusion_windows::is_ads_webview_occluded(
hwnd,
position.x,
position.y,
size.width,
size.height,
))
}
}
#[cfg(any(windows, target_os = "macos"))]
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
let Some(occluded) = compute_ads_webview_occlusion(app) else {
return;
};
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
if state.occluded == occluded {
return;
}
state.occluded = occluded;
let visible = state.shown && !state.modal_shown;
drop(state);
if let Some(webview) = app.webviews().get("ads-window") {
set_webview_visible_for_window(app, webview, visible);
}
}
fn sync_webview_visibility_for_main_window<R: Runtime>(
@@ -140,7 +221,7 @@ fn sync_webview_visibility_for_main_window<R: Runtime>(
false
} else {
match app.state::<RwLock<AdsState>>().try_read() {
Ok(state) => state.shown && !state.modal_shown,
Ok(state) => state.shown && !state.modal_shown && !state.occluded,
Err(_) => false,
}
};
@@ -173,20 +254,28 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
app.manage(RwLock::new(AdsState {
shown: true,
modal_shown: false,
occluded: false,
last_click: None,
malicious_origins: HashSet::new(),
}));
// We refresh the ads window every 5 minutes to mitigate memory leak issues.
// While this loop doesn't include explicit checks to see if the window is still
// visible when we refresh, the Aditude wrapper will not make any ad requests
// unless Chromium reports the page as visible. The refresh does not reset the
// visibility state.
// We refresh the ads window periodically to mitigate memory leak issues.
// Skip refreshes when app state has hidden the ads WebView. The refresh does
// not reset the visibility state.
let refresh_app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
if let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
let should_refresh = refresh_app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| {
state.shown && !state.modal_shown && !state.occluded
})
.unwrap_or(false);
if should_refresh
&& let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(AD_LINK.parse().unwrap());
}
@@ -224,6 +313,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
});
}
#[cfg(any(windows, target_os = "macos"))]
{
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
loop {
sync_ads_occlusion(&app_handle).await;
tokio::time::sleep(Duration::from_millis(200)).await;
}
});
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
+236
View File
@@ -0,0 +1,236 @@
use std::ptr::NonNull;
use core_foundation::array::CFArray;
use core_foundation::base::{CFType, TCFType};
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_graphics::display::CGDisplay;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_graphics::window::{
CGWindowID, kCGWindowAlpha, kCGWindowBounds, kCGWindowLayer,
kCGWindowListExcludeDesktopElements,
kCGWindowListOptionOnScreenAboveWindow, kCGWindowListOptionOnScreenOnly,
kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID,
};
use objc2_app_kit::NSWindow;
pub fn is_ads_webview_occluded<R: tauri::Runtime>(
main_window: &tauri::Window<R>,
ad_x: i32,
ad_y: i32,
ad_width: u32,
ad_height: u32,
) -> Option<bool> {
let scale_factor = main_window.scale_factor().ok()?;
let maybe_ns_window = main_window.ns_window().ok()?;
let ns_window = NonNull::new(maybe_ns_window)?.cast::<NSWindow>();
let ns_window = unsafe { ns_window.as_ref() };
let window_number = ns_window.windowNumber();
let main_window_id = if window_number > 0 {
window_number as CGWindowID
} else {
return None;
};
let window_infos = CGDisplay::window_list_info(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
None,
)?;
let window_infos = unsafe {
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
window_infos.as_concrete_TypeRef(),
)
};
let main_window_rect = window_infos
.iter()
.find(|window_info| window_id(window_info) == Some(main_window_id))
.and_then(|window_info| window_rect(&window_info))?;
let ad_rect = ad_rect_from_main_window(
&main_window_rect,
ad_x,
ad_y,
ad_width,
ad_height,
scale_factor,
);
if is_empty_rect(&ad_rect) {
return None;
}
let ad_area = rect_area(&ad_rect);
if ad_area == 0.0 {
return None;
}
let app_process_id = std::process::id() as i32;
let windows_above_main = CGDisplay::window_list_info(
kCGWindowListOptionOnScreenAboveWindow
| kCGWindowListExcludeDesktopElements,
Some(main_window_id),
)?;
let windows_above_main = unsafe {
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
windows_above_main.as_concrete_TypeRef(),
)
};
let mut occluded_area = 0.0;
for window_info in windows_above_main.iter() {
if window_id(&window_info) == Some(main_window_id) {
continue;
}
if window_process_id(&window_info) == Some(app_process_id) {
continue;
}
let owner_name = window_owner_name(&window_info);
if owner_name.as_deref().is_some_and(is_system_window_owner) {
continue;
}
let layer = window_layer(&window_info);
if layer != Some(0) {
continue;
}
let alpha = window_alpha(&window_info);
if alpha.is_some_and(|alpha| alpha <= 0.0) {
continue;
}
let Some(rect) = window_rect(&window_info) else {
continue;
};
let Some(intersection) = intersect_rects(&ad_rect, &rect) else {
continue;
};
occluded_area += rect_area(&intersection);
let occluded_ratio = occluded_area / ad_area;
if occluded_ratio >= super::ads::OCCLUDED_AREA_THRESHOLD {
return Some(true);
}
}
Some(false)
}
fn ad_rect_from_main_window(
main_window_rect: &CGRect,
x: i32,
y: i32,
width: u32,
height: u32,
scale_factor: f64,
) -> CGRect {
let scale_factor = if scale_factor > 0.0 {
scale_factor
} else {
1.0
};
CGRect::new(
&CGPoint::new(
main_window_rect.origin.x + x as f64 / scale_factor,
main_window_rect.origin.y + y as f64 / scale_factor,
),
&CGSize::new(width as f64 / scale_factor, height as f64 / scale_factor),
)
}
fn window_id(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<CGWindowID> {
number_value(window_info, unsafe { kCGWindowNumber })
.and_then(|value| u32::try_from(value).ok())
}
fn window_process_id(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<i32> {
number_value(window_info, unsafe { kCGWindowOwnerPID })
.and_then(|value| i32::try_from(value).ok())
}
fn window_layer(window_info: &CFDictionary<CFString, CFType>) -> Option<i32> {
number_value(window_info, unsafe { kCGWindowLayer })
.and_then(|value| i32::try_from(value).ok())
}
fn window_alpha(window_info: &CFDictionary<CFString, CFType>) -> Option<f64> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowAlpha) };
window_info.find(&key)?.downcast::<CFNumber>()?.to_f64()
}
fn window_owner_name(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<String> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowOwnerName) };
Some(window_info.find(&key)?.downcast::<CFString>()?.to_string())
}
fn is_system_window_owner(owner_name: &str) -> bool {
matches!(owner_name, "WindowManager")
}
fn number_value(
window_info: &CFDictionary<CFString, CFType>,
key: core_foundation::string::CFStringRef,
) -> Option<i64> {
let key = unsafe { CFString::wrap_under_get_rule(key) };
window_info.find(&key)?.downcast::<CFNumber>()?.to_i64()
}
fn window_rect(window_info: &CFDictionary<CFString, CFType>) -> Option<CGRect> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowBounds) };
let bounds = window_info.find(&key)?.downcast::<CFDictionary>()?;
let rect = CGRect::from_dict_representation(&bounds)?;
if is_empty_rect(&rect) {
None
} else {
Some(rect)
}
}
fn is_empty_rect(rect: &CGRect) -> bool {
rect.size.width <= 0.0 || rect.size.height <= 0.0
}
fn rect_area(rect: &CGRect) -> f64 {
if is_empty_rect(rect) {
return 0.0;
}
rect.size.width * rect.size.height
}
fn intersect_rects(a: &CGRect, b: &CGRect) -> Option<CGRect> {
let left = a.origin.x.max(b.origin.x);
let top = a.origin.y.max(b.origin.y);
let right = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
let bottom = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
let rect = CGRect::new(
&CGPoint::new(left, top),
&CGSize::new(right - left, bottom - top),
);
if is_empty_rect(&rect) {
None
} else {
Some(rect)
}
}
+191
View File
@@ -0,0 +1,191 @@
use windows::Win32::Foundation::{HWND, POINT, RECT};
use windows::Win32::Graphics::Dwm::{
DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute,
};
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::UI::WindowsAndMessaging::{
GA_ROOT, GW_HWNDNEXT, GetAncestor, GetTopWindow, GetWindow, GetWindowRect,
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
};
pub fn is_ads_webview_occluded(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> bool {
let Some(ad_rect) = ad_rect_in_screen(main_hwnd, x, y, width, height)
else {
return false;
};
if is_empty_rect(&ad_rect) {
return false;
}
let ad_area = rect_area(&ad_rect);
if ad_area == 0 {
return false;
}
let mut occluded_area = 0u64;
let app_root = unsafe { GetAncestor(main_hwnd, GA_ROOT) };
let app_process_id = std::process::id();
let mut hwnd = match unsafe { GetTopWindow(None) } {
Ok(hwnd) => hwnd,
Err(_) => return false,
};
while !hwnd.is_invalid() {
let window_root = unsafe { GetAncestor(hwnd, GA_ROOT) };
if window_root == app_root {
return false;
}
if window_process_id(hwnd) == Some(app_process_id) {
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
continue;
}
if window_counts_as_occluder(hwnd)
&& let Some(occluder_rect) = window_rect(hwnd)
&& let Some(intersection) =
intersect_rects(&ad_rect, &occluder_rect)
{
occluded_area =
occluded_area.saturating_add(rect_area(&intersection));
if (occluded_area as f64 / ad_area as f64)
>= super::ads::OCCLUDED_AREA_THRESHOLD
{
return true;
}
}
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
}
false
}
fn ad_rect_in_screen(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Option<RECT> {
let mut origin = POINT { x: 0, y: 0 };
if !unsafe { ClientToScreen(main_hwnd, &mut origin).as_bool() } {
return None;
}
let left = origin.x.saturating_add(x);
let top = origin.y.saturating_add(y);
let right = left.saturating_add(width as i32);
let bottom = top.saturating_add(height as i32);
Some(RECT {
left,
top,
right,
bottom,
})
}
fn window_counts_as_occluder(hwnd: HWND) -> bool {
if !unsafe { IsWindowVisible(hwnd).as_bool() } {
return false;
}
if unsafe { IsIconic(hwnd).as_bool() } {
return false;
}
if is_dwm_cloaked(hwnd) {
return false;
}
true
}
fn window_process_id(hwnd: HWND) -> Option<u32> {
let mut process_id = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
}
(process_id != 0).then_some(process_id)
}
fn is_dwm_cloaked(hwnd: HWND) -> bool {
let mut cloaked = 0u32;
unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
&mut cloaked as *mut u32 as *mut _,
std::mem::size_of::<u32>() as u32,
)
}
.is_ok()
&& cloaked != 0
}
fn window_rect(hwnd: HWND) -> Option<RECT> {
let mut rect = RECT::default();
if unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
&mut rect as *mut RECT as *mut _,
std::mem::size_of::<RECT>() as u32,
)
}
.is_err()
&& unsafe { GetWindowRect(hwnd, &mut rect) }.is_err()
{
return None;
}
if is_empty_rect(&rect) {
return None;
}
Some(rect)
}
fn is_empty_rect(rect: &RECT) -> bool {
rect.right <= rect.left || rect.bottom <= rect.top
}
fn rect_area(rect: &RECT) -> u64 {
if is_empty_rect(rect) {
return 0;
}
(rect.right - rect.left) as u64 * (rect.bottom - rect.top) as u64
}
fn intersect_rects(a: &RECT, b: &RECT) -> Option<RECT> {
let rect = RECT {
left: a.left.max(b.left),
top: a.top.max(b.top),
right: a.right.min(b.right),
bottom: a.bottom.min(b.bottom),
};
(!is_empty_rect(&rect)).then_some(rect)
}
+51 -17
View File
@@ -11,10 +11,12 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
get_available_capes,
get_available_skins,
add_and_equip_custom_skin,
set_default_cape,
equip_skin,
remove_custom_skin,
save_custom_skin,
unequip_skin,
flush_pending_skin_change,
flush_pending_skin_change_for_profile,
normalize_skin_texture,
get_dragged_skin_data,
])
@@ -37,29 +39,19 @@ pub async fn get_available_skins() -> Result<Vec<Skin>> {
Ok(minecraft_skins::get_available_skins().await?)
}
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape_override)`
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape)`
///
/// See also: [minecraft_skins::add_and_equip_custom_skin]
#[tauri::command]
pub async fn add_and_equip_custom_skin(
texture_blob: Bytes,
variant: MinecraftSkinVariant,
cape_override: Option<Cape>,
) -> Result<()> {
Ok(minecraft_skins::add_and_equip_custom_skin(
texture_blob,
variant,
cape_override,
cape: Option<Cape>,
) -> Result<Skin> {
Ok(
minecraft_skins::add_and_equip_custom_skin(texture_blob, variant, cape)
.await?,
)
.await?)
}
/// `invoke('plugin:minecraft-skins|set_default_cape', cape)`
///
/// See also: [minecraft_skins::set_default_cape]
#[tauri::command]
pub async fn set_default_cape(cape: Option<Cape>) -> Result<()> {
Ok(minecraft_skins::set_default_cape(cape).await?)
}
/// `invoke('plugin:minecraft-skins|equip_skin', skin)`
@@ -78,6 +70,27 @@ pub async fn remove_custom_skin(skin: Skin) -> Result<()> {
Ok(minecraft_skins::remove_custom_skin(skin).await?)
}
/// `invoke('plugin:minecraft-skins|save_custom_skin', skin, texture_blob, variant, cape, replace_texture)`
///
/// See also: [minecraft_skins::save_custom_skin]
#[tauri::command]
pub async fn save_custom_skin(
skin: Skin,
texture_blob: Bytes,
variant: MinecraftSkinVariant,
cape: Option<Cape>,
replace_texture: bool,
) -> Result<Skin> {
Ok(minecraft_skins::save_custom_skin(
skin,
texture_blob,
variant,
cape,
replace_texture,
)
.await?)
}
/// `invoke('plugin:minecraft-skins|unequip_skin')`
///
/// See also: [minecraft_skins::unequip_skin]
@@ -86,6 +99,27 @@ pub async fn unequip_skin() -> Result<()> {
Ok(minecraft_skins::unequip_skin().await?)
}
/// `invoke('plugin:minecraft-skins|flush_pending_skin_change')`
///
/// See also: [minecraft_skins::flush_pending_skin_change]
#[tauri::command]
pub async fn flush_pending_skin_change() -> Result<()> {
Ok(minecraft_skins::flush_pending_skin_change().await?)
}
/// `invoke('plugin:minecraft-skins|flush_pending_skin_change_for_profile', profile_id)`
///
/// See also: [minecraft_skins::flush_pending_skin_change_for_profile]
#[tauri::command]
pub async fn flush_pending_skin_change_for_profile(
profile_id: uuid::Uuid,
) -> Result<()> {
Ok(
minecraft_skins::flush_pending_skin_change_for_profile(profile_id)
.await?,
)
}
/// `invoke('plugin:minecraft-skins|normalize_skin_texture')`
///
/// See also: [minecraft_skins::normalize_skin_texture]
+4
View File
@@ -18,6 +18,10 @@ pub mod tags;
pub mod utils;
pub mod ads;
#[cfg(target_os = "macos")]
mod ads_occlusion_macos;
#[cfg(windows)]
mod ads_occlusion_windows;
pub mod cache;
pub mod files;
pub mod friends;
+12 -2
View File
@@ -270,10 +270,20 @@ fn main() {
Ok(app) => {
app.run(|app, event| {
#[cfg(not(any(feature = "updater", target_os = "macos")))]
drop((app, event));
let _ = app;
if matches!(&event, tauri::RunEvent::ExitRequested { .. })
&& let Err(error) = tauri::async_runtime::block_on(
theseus::minecraft_skins::flush_pending_skin_change(),
)
{
tracing::warn!(
"Failed to flush pending Minecraft skin change before exit: {error}"
);
}
#[cfg(feature = "updater")]
if matches!(event, tauri::RunEvent::Exit) {
if matches!(&event, tauri::RunEvent::Exit) {
let update_data = app.state::<PendingUpdateData>().inner();
let should_restart = State::get_if_initialized()
.map(|s| {
+53 -36
View File
@@ -65,49 +65,65 @@ async fn fetch(
)
.await?;
// We check Modrinth's fabric version manifest and compare if the fabric version exists in Modrinth's database
// We also check intermediary versions that are newly added to query
let (fetch_fabric_versions, fetch_intermediary_versions) =
if let Some(modrinth_manifest) = modrinth_manifest {
let (mut fetch_versions, mut fetch_intermediary_versions) =
(Vec::new(), Vec::new());
// We check Modrinth's manifest to find newly added loader versions,
// intermediary/mapping artifacts, and game versions.
let (
fetch_fabric_versions,
fetch_intermediary_versions,
has_new_game_versions,
) = if let Some(modrinth_manifest) = modrinth_manifest {
let (mut fetch_versions, mut fetch_intermediary_versions) =
(Vec::new(), Vec::new());
for version in &fabric_manifest.loader {
if !modrinth_manifest
.game_versions
.iter()
.any(|x| x.loaders.iter().any(|x| x.id == version.version))
&& !skip_versions.contains(&&*version.version)
{
fetch_versions.push(version);
}
for version in &fabric_manifest.loader {
if !modrinth_manifest
.game_versions
.iter()
.any(|x| x.loaders.iter().any(|x| x.id == version.version))
&& !skip_versions.contains(&&*version.version)
{
fetch_versions.push(version);
}
}
for version in &fabric_manifest.intermediary {
if !modrinth_manifest
for version in &fabric_manifest.intermediary {
if !modrinth_manifest
.game_versions
.iter()
.any(|x| x.id == version.version)
&& fabric_manifest
.game
.iter()
.any(|x| x.version == version.version)
{
fetch_intermediary_versions.push(version);
}
}
let has_new_game_versions =
fabric_manifest.game.iter().any(|version| {
!modrinth_manifest
.game_versions
.iter()
.any(|x| x.id == version.version)
&& fabric_manifest
.game
.iter()
.any(|x| x.version == version.version)
{
fetch_intermediary_versions.push(version);
}
}
});
(fetch_versions, fetch_intermediary_versions)
} else {
(
fabric_manifest
.loader
.iter()
.filter(|x| !skip_versions.contains(&&*x.version))
.collect(),
fabric_manifest.intermediary.iter().collect(),
)
};
(
fetch_versions,
fetch_intermediary_versions,
has_new_game_versions,
)
} else {
(
fabric_manifest
.loader
.iter()
.filter(|x| !skip_versions.contains(&&*x.version))
.collect(),
fabric_manifest.intermediary.iter().collect(),
true,
)
};
const DUMMY_GAME_VERSION: &str = "1.21";
@@ -216,6 +232,7 @@ async fn fetch(
if !fetch_fabric_versions.is_empty()
|| !fetch_intermediary_versions.is_empty()
|| has_new_game_versions
{
let fabric_manifest_path =
format!("{mod_loader}/v{format_version}/manifest.json",);
-1
View File
@@ -40,4 +40,3 @@ These composables are deprecated and should not be used in new code:
- **`useAsyncData`** - we use tanstack, not nuxt's built in async data utility.
- **`useBaseFetch`** (`src/composables/fetch.js`) — legacy Labrinth fetch wrapper. Use `client.labrinth.*` modules instead.
- **`useServersFetch`** (`src/composables/servers/servers-fetch.ts`) — legacy Archon fetch wrapper with manual retry/circuit-breaker. Use `client.archon.*` modules instead — refer to the `packages/api-client/CLAUDE.md` for more information.
+20 -16
View File
@@ -1,10 +1,15 @@
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
import serverSidedVue from '@vitejs/plugin-vue'
import fs from 'fs/promises'
import { defineNuxtConfig } from 'nuxt/config'
import { fileURLToPath } from 'url'
import svgLoader from 'vite-svg-loader'
import { GenericModrinthClient, type Labrinth } from '../../packages/api-client/src/index.ts'
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
const API_CLIENT_SOURCE = fileURLToPath(
new URL('../../packages/api-client/src/index.ts', import.meta.url),
)
const preloadedFonts = [
'inter/Inter-Regular.woff2',
@@ -24,6 +29,9 @@ const STAGING_MODRINTH_URL = 'https://staging.modrinth.com'
export default defineNuxtConfig({
srcDir: 'src/',
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
app: {
head: {
htmlAttrs: {
@@ -67,10 +75,10 @@ export default defineNuxtConfig({
},
ssr: {
// https://github.com/Akryum/floating-vue/issues/809#issuecomment-1002996240
noExternal: ['v-tooltip'],
optimizeDeps: {
include: ['vue-router'],
},
noExternal: ['floating-vue', '@floating-ui/core', '@floating-ui/dom'],
},
optimizeDeps: {
include: ['vue-router', 'floating-vue', '@floating-ui/dom'],
},
define: {
global: {},
@@ -82,6 +90,9 @@ export default defineNuxtConfig({
},
cacheDir: '../../node_modules/.vite/apps/knossos',
resolve: {
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
dedupe: ['vue'],
},
plugins: [
@@ -93,6 +104,9 @@ export default defineNuxtConfig({
params: {
overrides: {
removeViewBox: false,
cleanupIds: {
minify: false,
},
},
},
},
@@ -114,8 +128,6 @@ export default defineNuxtConfig({
const docTemplates = Object.keys(
await import('./src/templates/docs/index.ts').then((m) => m.default),
)
const blogArticles = await import('@modrinth/blog').then((m) => m.articles)
const { getChangelog } = await import('@modrinth/blog')
nitroConfig.prerender = nitroConfig.prerender || {}
nitroConfig.prerender.routes = nitroConfig.prerender.routes || []
@@ -125,15 +137,6 @@ export default defineNuxtConfig({
for (const template of docTemplates) {
nitroConfig.prerender.routes.push(`/_internal/templates/doc/${template}`)
}
nitroConfig.prerender.routes.push('/news')
for (const article of blogArticles) {
nitroConfig.prerender.routes.push(`/news/article/${article.slug}`)
}
nitroConfig.prerender.routes.push('/news/changelog')
for (const entry of getChangelog()) {
const id = entry.version ?? entry.date.unix()
nitroConfig.prerender.routes.push(`/news/changelog/${entry.product}/${id}`)
}
},
async 'build:before'() {
// 30 minutes
@@ -221,6 +224,7 @@ export default defineNuxtConfig({
globalThis.INTERCOM_APP_ID ||
'ykeritl9',
production: isProduction(),
cookieSecure: isProduction(),
buildEnv: process.env.BUILD_ENV,
preview: process.env.PREVIEW === 'true',
featureFlagOverrides: getFeatureFlagOverrides(),
+3 -2
View File
@@ -11,6 +11,7 @@
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"src/{components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/frontend",
"cf-deploy": "pnpm run build && wrangler deploy --env staging",
"cf-dev": "pnpm run build && wrangler dev --env staging",
"cf-typegen": "wrangler types"
@@ -25,7 +26,7 @@
"@types/semver": "^7.7.1",
"autoprefixer": "^10.4.19",
"glob": "^10.2.7",
"nuxt": "^3.20.2",
"nuxt": "=3.20.2",
"postcss": "^8.4.39",
"prettier-plugin-tailwindcss": "^0.6.5",
"sass": "^1.58.0",
@@ -38,7 +39,6 @@
},
"dependencies": {
"@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:*",
@@ -55,6 +55,7 @@
"@vueuse/core": "^11.1.0",
"ace-builds": "^1.36.2",
"ansi-to-html": "^0.7.2",
"chart.js": "^4.5.1",
"dayjs": "^1.11.7",
"dompurify": "^3.1.7",
"floating-vue": "^5.2.2",
@@ -1,18 +1,18 @@
/*
Cards and body styling
*/
// CARDS
.base-card {
padding: var(--spacing-card-lg);
padding: 1rem;
position: relative;
min-height: var(--font-size-2xl);
background-color: var(--color-raised-bg);
border-radius: var(--size-rounded-card);
background-color: var(--surface-3);
border-radius: var(--radius-lg);
border: 1px solid var(--surface-4);
margin-bottom: var(--spacing-card-md);
margin-bottom: var(--gap-md);
outline: 2px solid transparent;
outline-offset: -2px;
box-shadow: var(--shadow-card);
.card__overlay {
position: absolute;
@@ -25,6 +25,17 @@
z-index: 2;
}
&:where(&.warning, &.information) {
padding: 1.5rem;
line-height: 1.5;
min-height: 0;
a {
color: var(--color-blue);
text-decoration: underline;
}
}
&.moderation-card {
background-color: var(--color-warning-banner-bg);
}
@@ -50,6 +50,7 @@
@media screen and (max-width: 1024px) {
margin-top: 1.5rem;
padding: 0 1rem;
}
.normal-page__sidebar {
@@ -0,0 +1,152 @@
<template>
<div class="analytics-loading-bar" :style="{ opacity: isVisible ? 1 : 0 }" aria-hidden="true">
<div
class="analytics-loading-bar__track"
:style="{
width: `${progress}%`,
transition: !isTransitioning
? 'none'
: isFinishing
? 'width 0.1s ease-in-out'
: isCreeping
? 'width 2s linear'
: 'width 0.9s ease-in-out',
}"
/>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
const props = defineProps<{
loading: boolean
}>()
const progress = ref(0)
const isVisible = ref(false)
const isFinishing = ref(false)
const isCreeping = ref(false)
const isTransitioning = ref(false)
let startFrame: number | null = null
let showFrame: number | null = null
let creepTimeout: ReturnType<typeof setTimeout> | null = null
let hideTimeout: ReturnType<typeof setTimeout> | null = null
let resetTimeout: ReturnType<typeof setTimeout> | null = null
function clearTimers() {
if (showFrame !== null && typeof window !== 'undefined') {
window.cancelAnimationFrame(showFrame)
}
if (startFrame !== null && typeof window !== 'undefined') {
window.cancelAnimationFrame(startFrame)
}
if (creepTimeout) clearTimeout(creepTimeout)
if (hideTimeout) clearTimeout(hideTimeout)
if (resetTimeout) clearTimeout(resetTimeout)
showFrame = null
startFrame = null
creepTimeout = null
hideTimeout = null
resetTimeout = null
}
function start() {
clearTimers()
isVisible.value = false
progress.value = 0
isFinishing.value = false
isCreeping.value = false
isTransitioning.value = false
if (typeof window === 'undefined') {
progress.value = 98
return
}
showFrame = window.requestAnimationFrame(() => {
isVisible.value = true
showFrame = null
startFrame = window.requestAnimationFrame(() => {
isTransitioning.value = true
progress.value = 85
startFrame = null
})
})
creepTimeout = setTimeout(() => {
isCreeping.value = true
progress.value = 98
creepTimeout = null
}, 900)
}
function finish() {
clearTimers()
isVisible.value = true
isFinishing.value = true
isCreeping.value = false
isTransitioning.value = true
progress.value = 100
if (typeof window === 'undefined') {
isVisible.value = false
progress.value = 0
isFinishing.value = false
isCreeping.value = false
isTransitioning.value = false
return
}
hideTimeout = setTimeout(() => {
isVisible.value = false
resetTimeout = setTimeout(() => {
isTransitioning.value = false
progress.value = 0
isFinishing.value = false
isCreeping.value = false
}, 400)
}, 350)
}
watch(
() => props.loading,
(loading) => {
if (loading) {
start()
} else if (
isVisible.value ||
progress.value > 0 ||
showFrame !== null ||
startFrame !== null ||
creepTimeout !== null
) {
finish()
}
},
{ immediate: true },
)
onBeforeUnmount(clearTimers)
</script>
<style scoped>
.analytics-loading-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 20;
height: 2px;
overflow: hidden;
background: color-mix(in srgb, var(--color-brand) 18%, transparent);
pointer-events: none;
transition: opacity 0.4s;
}
.analytics-loading-bar__track {
height: 100%;
border-radius: 999px;
background: var(--loading-bar-gradient);
}
</style>

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