Compare commits

...
Author SHA1 Message Date
tdgao bec0cdec23 fix:icon too bright 2026-02-08 13:17:40 -07:00
Prospector 57e012f9b7 Revert "Fix author name underline + hover icon brightness"
This reverts commit 8ab1895d8a.
2026-02-07 14:05:35 -08:00
Prospector 8ab1895d8a Fix author name underline + hover icon brightness 2026-02-07 13:51:18 -08:00
Prospector 13e5529f00 add project status to grid cards 2026-02-07 12:28:06 -08:00
Prospector 428efde36d proj card fixes 2026-02-07 12:05:59 -08:00
Prospector a978873bff changelog 2026-02-07 11:36:52 -08:00
b005c1f522 New project cards (#5298)
* New project card

* no shadow on icons

* Remove updated label

* reduce tag count to 5

* improve envs

* fix: project card bottom row not growing

* move actions in grid mode

* focus changes + new project list component

* Allow more tags in grid mode, deprioritize non-loader tags

* fix prod deploy robots.txt

* remove unused id

* App cards

* prepr

* publish date + fix router links

* fix author hover underline in firefox

* perf: preload on search item hover

* remove unused filter

* remove option for old grid view

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2026-02-07 11:18:59 -08:00
Prospector b6c22d6ca6 changelog 2026-02-07 10:28:59 -08:00
Calum H.andGitHub 50064c4ed6 qa: tech review 3 (#5250)
* fix: only collapse if pending -> pass/fail, not pass <-> fail

* feat: wrap in full details block

* feat: left badge

* feat: in mod queue -> in project queue

* fix: hash on malic modal

* feat: remove return to queue on indiv page

* fix: truncate in middle

* feat: bulk actions

* fix: reactivity problem

* feat: project page dropdown option

* feat: show metadata if exists

* fix: lint

* fix: qa problems

* feat: debug logging for malicious summary modal

* fix: lint

* qa: go back on bulk

* fix: reactive sets/maps -> refs

* fix: lint
2026-02-07 18:10:58 +00:00
ProspectorandGitHub 48248eafdc Fix app releases not being signed (#5317) 2026-02-06 23:19:40 +00:00
b34564a770 Fix editing project team member permissions (#5315)
* Fix editing project team member permissions

* prepare

* add success notifications

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
2026-02-06 14:56:36 -08:00
ProspectorandGitHub d713cea180 fix instance page in app (#5316) 2026-02-06 14:53:57 -08:00
JerozgenandGitHub 695233e736 Add ICU select to report strings (#5312) 2026-02-06 18:27:55 +00:00
Calum H.andGitHub 8789d7b057 fix: 5258 (#5310)
* fix: 5258

* fix: lint
2026-02-06 18:21:41 +00:00
lumiscosityandGitHub 510ea6cde4 Enable support for Filipino, Indonesian, Korean, Dutch, and Vietnamese (#5305)
All five of these have now crossed 80% translation completion!

Signed-off-by: lumiscosity <averyrudelphe@gmail.com>
2026-02-05 14:52:38 +00:00
aecsocketandGitHub b1954be2c7 Ref-count Redis pool internals, fix project creation slug/ID collision (#5302)
* Ref-count Redis pool internals, fix project creation slug/ID collision

* cargo sqlx prepare
2026-02-05 05:18:33 +00:00
Prospector 9105a68923 changelog 2026-02-04 15:54:16 -08:00
Prospector 06e2f59a94 changelog 2026-02-04 14:54:17 -08:00
ProspectorandGitHub ddb013e024 translatable category headers (#5301) 2026-02-04 14:47:35 -08:00
aecsocketandGitHub 3f5e3b1d8b Disable login captcha if backend has no captcha secret (#5288)
* Add /_internal/globals route

* Don't show login captcha if backend claims it's disabled

* try to re-add tombi

* typos

* Assume captcha enabled if globals route is unreachable

* Prepare frontend fixes
2026-02-04 18:08:14 +00:00
323090966b feat: app server projects modals + modal borders (#5256)
* feat: add modals

* NewModal add stroke

* update diff type sorting

* update icon to match figma

* fix lint ci issues

* remove formatCategory

* feature flag on buttons

* prepr

* consistent modal borders

* intl

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-02-04 07:27:25 -08:00
Calum H.andGitHub 16204d30f8 fix: withdraw flow fixes (#5296)
* fix: dev-741 currency exchanging bug

* fix: remove redundant balance available check

* fix: lint/fmt

* fix: #5245

* fix: hide max if it's less than min
2026-02-04 14:56:14 +00:00
JerozgenandGitHub 34cbc7e0c1 Use numeric: always for Italian and Russian (#5293)
* Use `numeric: always` for Italian and Russian

* Use RelativeTimeFormatNumeric type
2026-02-04 13:49:21 +00:00
aecsocketandGitHub 5d6593a9da Add more Prometheus metrics for memory and Tokio tasks (#5282)
* Add more Prometheus metrics for memory and Tokio tasks

* pr comments
2026-02-03 19:05:34 +00:00
Prospector ab753a82bc changelog 2026-02-03 09:11:50 -08:00
Truman GaoandGitHub 880e759336 fix: ensure legacy texture pack is checking .zip file extension (#5284) 2026-02-03 04:38:56 +00:00
Prospector 84b386141c Fix formatLoader 2026-02-02 16:57:15 -08:00
Prospector e9a4cc60ca update ads.txt 2026-02-02 16:55:44 -08:00
aecsocketandGitHub d5869c514e Use deadpool fork with tracing (#5276)
* Use deadpool fork with tracing

* Implement minimum Redis connections

* fix typos maybe

* address pr comments
2026-02-02 23:24:55 +00:00
Prospector de0a03b2e9 changelog 2026-02-02 13:17:22 -08:00
3f6c79b00d Application & pat ui improvements (#5271)
* Add categories, make localizable

* Run fix

* Run prepr

* Improve pat modal ui

* Fix pat token actions

* Make scope category localization shared

* Fix casing

* Fix casing

---------

Co-authored-by: Creeperkatze <178587183+Creeperkatze@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-02-02 18:20:28 +00:00
ProspectorandGitHub 56c8bb1950 Remove legacy formatCategory (#5280)
* Remove legacy formatCategory

* prepr
2026-02-02 18:20:23 +00:00
François-Xavier TalbotandGitHub f81f951814 Don't persist read search client across reqs (#5277) 2026-02-02 14:09:00 +00:00
François-Xavier TalbotandGitHub 345ada27c0 Increase/make configurable search timeouts (#5261)
* Increase default operation timeout, make it configurable via SEARCH_OPERATION_TIMEOUT

* Don't update index settings when unneeded

* Add to env local
2026-02-02 13:27:29 +00:00
François-Xavier TalbotandGitHub e3395a7366 Development app builds (#5255)
* Workflow changes

* Propagate app_identifier via State

* Remove old updater stuff

* Remove patch file

* Remove unused binding

* Fix application loading

* Don't sign windows binaries by default

* Remove the damn emojis

* Figure out where windows artifacts are

* Fix windows artifacts
2026-02-02 13:10:23 +00:00
Modrinth BotandGitHub 3552c8280b New translations from Crowdin (main) (#5274) 2026-02-02 09:49:27 +00:00
ProspectorandGitHub 11f00be606 Clean up formatters from prev PR (#5254) 2026-02-02 09:38:36 +00:00
aecsocketandGitHub a207daef0d Update charges rows on user deletion (#5269) 2026-02-01 19:14:38 +00:00
aecsocketandGitHub ddd395ed4f Fix FX conversion for Tremendous methods (#5268) 2026-02-01 19:05:32 +00:00
Prospector 04a7ae55e7 prepr 2026-01-30 15:18:38 -08:00
Prospector 5a33d462f6 changelog 2026-01-30 13:05:32 -08:00
ProspectorandGitHub cefa7b90ed Add noindex to pages that should be private (#5257)
* Add noindex to pages that should be private

* fix unused
2026-01-30 13:04:19 -08:00
NotchArrowandGitHub 0875a8a0bc Fixed Payout Source Code Link (#5251) 2026-01-30 16:51:20 +00:00
Calum H.andGitHub 5624e86c2b fix: stale cache handling (#5253)
* fix: allow stale cache

* fix: cache amnt

* fix: other one asw
2026-01-30 16:41:03 +00:00
Prospector c622a9281f changelog 2026-01-30 07:51:57 -08:00
Prospector 937d385241 Revert "remove imgur from proxy whitelist (#5239)"
This reverts commit deaa57fa15.
2026-01-30 07:50:38 -08:00
Calum H.andGitHub 3ec9bcf7e7 fix: caching problems frontend for version upload (#5252) 2026-01-30 14:50:57 +00:00
Prospector 3dd5314062 Add loading state to envs 2026-01-28 17:52:32 -08:00
Prospector d77823c5f4 Fix reference to props on members page 2026-01-28 17:19:51 -08:00
Prospector 41b48bd353 changelog 2026-01-28 16:42:42 -08:00
6225ece6be Add Tangled to common source links (#5236)
* Add Tangled to common source links

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-01-28 16:41:31 -08:00
ProspectorandGitHub 3f8805b953 fix some issues with latest PRs (#5242) 2026-01-28 16:41:17 -08:00
ProspectorandGitHub 6d68c0983f fix quilt svg (#5241) 2026-01-28 16:35:28 -08:00
Prospector 3ac0eb896c changelog 2026-01-28 13:54:54 -08:00
Truman GaoandGitHub 9c309e6da2 fix: various fixes for versions (#5238)
* fix: environment not showing when has mod loaders

* fix: weird snapshot format by only grouping consecutive tags with 3 or more

* fix: 26.1 snapshots not grouped properly in mc version picker

* remove debug console.log
2026-01-28 13:46:07 -08:00
ProspectorandGitHub deaa57fa15 remove imgur from proxy whitelist (#5239) 2026-01-28 13:45:37 -08:00
ProspectorandGitHub 8053fec29f Fix iris logo color (#5240) 2026-01-28 13:45:24 -08:00
78aca7e5c0 feat: shared components for worlds + p2p instances (#5135)
* feat: base content card component

* fix: tooltips + colors

* feat: fix orgs

* feat: add ContentModpackCard

* fix: extract types

* feat: selection v-model

* add show icon in selected for combobox with stories

* feat: add project combobox

* clean up project combobox

* feat: start install to play modal

* fix: events

* feat: figma alignments

* feat: migrate toggle to tailwind

* fix: row borders

* feat: disabled state

* feat: virtual list impl for card table based on window scroll

* fix: lint

* feat: virtualization + smaller contentcard items

* feat: fix gap + border issues on last elm

* fix: use TeleportOverflowMenu

* fix: hasUpdate type

* fix: fallback to svg if src is invalid on avatar component

* fix: storybook

* feat: start on updater modal

* feat: finish content updater modal

* feat: i18n pass

* remove install to play modal from ui package

* pnpm prepr

* feat: reusable table component

* feat: add column width prop for table and fix stories

* feat: add table overflow menu story example

* feat: add surface-1.5 and use in table

* chore: export table in index

* fix: allow more loose typing on columns

* feat: update table component to derive key from column instead of data

* feat: surface 1.5 for oled + refactor story for contentcardtable + yeet sorting funcs

* fix: lint

* feat: add no padding story for new modal

---------

Signed-off-by: Calum H. <contact@cal.engineer>
Co-authored-by: tdgao <mr.trumgao@gmail.com>
2026-01-28 20:09:24 +00:00
ProspectorandGitHub 728f8db7b9 Add game version warning for shaders (#5227) 2026-01-28 19:43:58 +00:00
Calum H.andGitHub 4c14339b4b fix: locale loading for ui + moderation nags (#5235)
* fix: locale loading

* fix: locale problems

* fix: lint
2026-01-28 19:41:03 +00:00
ProspectorandGitHub 16ac2aae6b Make tags translatable, move icons to frontend, a few other things (#5229)
* Make tags translatable, move icons to frontend, a few other things

* Migrate more things

* fix import

* more import fixes

* export tag-messages

* lint
2026-01-28 19:01:56 +00:00
6d68d50699 Deduplicate common strings in translation (#5085)
* deduplicate common strings, part 1

* deduplicate common strings, part 2

* typo and general import mess fixes

* detail common string

* fix lint

* fix lint TWO

* adress review concerns + lint

* app lint too

* actually leave privateLabel untouched

* lint fix THREE

* fix: broken msg

* fix: lint

---------

Co-authored-by: Calum H. <contact@cal.engineer>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-01-28 18:10:50 +00:00
Calum H.andGitHub 400c571fe6 refactor: project saving logic (#5225)
* fix: project data saving not visually shown immediately

* feat: useSavable improvements

* feat: migrate where possible to useSavable

* fix: gitignore

* feat: use es-toolkit
2026-01-28 16:46:14 +00:00
aecsocketandGitHub e57c15b3ce Add SQLx operation tracing (#5223)
* wip: vendor sqlx-tracing

* (compiles) standardize pg types used

* more standardization

* general log message improvements

* wip: improve sqlx-tracing architecture

* unify sqlx::Executor type

* wip: try fix sqlx tracing

* wip: sqlx-tracing compiles

* so close

* it compiles

* fix ci
2026-01-28 13:38:57 +00:00
Prospector 7cb7e881fa Revert "changelog"
This reverts commit 638bb55649.
2026-01-27 16:31:20 -08:00
Prospector 2cf82349a5 Revert "feat: badge for tax 2025 bug (#5230)"
This reverts commit 6d1b0eef15.
2026-01-27 16:31:15 -08:00
Prospector 638bb55649 changelog 2026-01-27 14:09:10 -08:00
Calum H.andGitHub 6d1b0eef15 feat: badge for tax 2025 bug (#5230) 2026-01-27 19:03:54 +00:00
03658b6a62 feat: show update available notif for modrinth app in linux (#5181)
* feat: show update available notif for modrinth app in linux

* remove changelog button

* update copy

* pnpm prepr

* add restart app changelog

* pnpm prepr

* use env var to check for updates are disabled

* update copy

* pnpm prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-01-27 14:20:11 +00:00
François-Xavier TalbotandGitHub f998a8dca5 Allow node transfer field to recv more than 1 node (#5220) 2026-01-26 22:49:21 +00:00
Modrinth BotandGitHub 0d6bee3a5b New translations from Crowdin (main) (#5217) 2026-01-26 07:47:29 +00:00
Prospector 7ce334aeb2 changelog 2026-01-25 13:22:14 -08:00
ProspectorandGitHub fe3a1b2058 Fix versions not loading in settings page (#5214)
* Fix versions not loading in settings page

* prepr
2026-01-25 13:21:25 -08:00
662b73211a Fix CSP issue when changing capes (#5177)
* Update tauri.conf.json

Signed-off-by: Felix <60808107+ItsFelix5@users.noreply.github.com>

* feat: add https for futureproofing asw

---------

Signed-off-by: Felix <60808107+ItsFelix5@users.noreply.github.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-01-25 20:59:06 +00:00
c453a03a02 fix: dont await lock releasing (#5161)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-01-25 20:57:43 +00:00
aecsocketandGitHub 05f8113bd0 Fix CI on main (#5213) 2026-01-25 12:40:22 -08:00
Prospector 63dfc3f636 changelog 2026-01-25 12:31:44 -08:00
Calum H.andGitHub c96a303d8a fix: optimistically updating on moderation mutations (#5200)
* fix: moderation optimistically update

* fix: gallery settings
2026-01-25 12:27:45 -08:00
François-Xavier TalbotandGitHub 2d7e87a4cb Local cache lock (#5193) 2026-01-24 00:35:46 +01:00
Prospector fa421b4b83 changelog 2026-01-23 12:59:29 -08:00
François-Xavier TalbotandGitHub 79217e78b4 Theseus circuit breaker (#5196)
* Impl request fence

* Reduce max FETCH_ATTEMPTS

* Tweak fail threshold & window

* Check block status every retry iteration

* Fix fmt

* Fix fmt 2
2026-01-23 20:28:50 +00:00
Prospector bdd808c279 changelog 2026-01-23 12:17:44 -08:00
986a7e6216 feat: ssr fixes + switch project page to tanstack (#5192)
* feat: ssr fixes

* feat: lazy load non-core data

* feat: ssr timing debugging

* feat: go back to all parallel

* feat: migrate to DI + set up mutators

* feat: remove double get versions request, only call v3

* refactor: [version].vue page to use composition API and typescript

* feat: gallery.vue start

* fix: remove left behind console log

* fix: type issues + gallery

* fix: versionsummary modal + version page direct join

* fix: projectRaw guard

* fix: currentMember val fix

* fix: actualProjectType

* fix: vers summary link same page

* fix: lint

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
2026-01-23 12:12:50 -08:00
lumiscosityandGitHub b54fcaa0b1 feat: Make hosting marketing page translatable (#5145)
* feat: make hosting marketing page translatable, part 1

* format what we've got so far

* lint and fix locale setting

* the rest of the owl, almost

still one more message in MedalPlanPromotion that's a bit annoying because of all the inline styles

* finishing touches

some things just shouldn't be questioned, i guess. that's two for two on issues that occur even though i seem to have done everything right. i give up

* whoops, that's literal

* get back in the span, you

* fix typo + lint

* and now it works

* one more fix
2026-01-23 19:54:24 +00:00
Michael H. 1cf782c298 Revert "Implement redis clustering (#5189)"
This reverts commit fb1050e409.
2026-01-23 16:08:07 +01:00
fb1050e409 Implement redis clustering (#5189)
Co-authored-by: Jai Agrawal <geometrically@Jais-MacBook-Pro.local>
2026-01-23 13:51:17 +01:00
François-Xavier TalbotandGitHub 5c29a8c7dd Batched search indexing (#5191)
* Use RO pool for search indexing

* Batched search indexing that actually works

* Query cache
2026-01-23 12:32:02 +00:00
592 changed files with 36516 additions and 15815 deletions
+11 -9
View File
@@ -13,13 +13,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
- uses: crate-ci/typos@v1.43.1
# broken: <https://github.com/SchemaStore/schemastore/issues/5108>
# tombi:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: tombi-toml/setup-tombi@v1
# - run: tombi lint
# - run: tombi fmt --check
# see <https://github.com/influxdata/datafusion-udf-wasm/pull/275>
tombi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@v2
with:
tool: tombi
- run: tombi lint
- run: tombi fmt --check
+39 -18
View File
@@ -19,7 +19,7 @@ on:
sign-windows-binaries:
description: Sign Windows binaries
type: boolean
default: true
default: false
required: false
jobs:
@@ -40,33 +40,48 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- name: 📥 Check out code
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🧰 Setup Rust toolchain
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ''
target: ${{ startsWith(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
- name: 🧰 Install pnpm
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: 🧰 Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- name: 🧰 Install Linux build dependencies
- name: Generate tauri-dev.conf.json
shell: bash
run: |
GIT_HASH=$(git rev-parse --short HEAD)
cat > apps/app/tauri-dev.conf.json <<EOF
{
"productName": "Modrinth App (dev-${GIT_HASH})",
"mainBinaryName": "Modrinth App (dev-${GIT_HASH})",
"identifier": "ModrinthApp-dev-${GIT_HASH}",
"bundle": {
"fileAssociations": []
}
}
EOF
- name: Install Linux build dependencies
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
- name: 🧰 Setup Dasel
- name: Setup Dasel
uses: jaxxstorm/action-install-gh-release@v2.1.0
with:
repo: TomWright/dasel
@@ -75,7 +90,7 @@ jobs:
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
chmod: 0755
- name: ⚙️ Set application version and environment
- name: Set application version and environment
shell: bash
run: |
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
@@ -86,13 +101,13 @@ jobs:
cp packages/app-lib/.env.prod packages/app-lib/.env
- name: 💨 Setup Turbo cache
- name: Setup Turbo cache
uses: rharkor/caching-for-turbo@v1.8
- name: 🧰 Install dependencies
- name: Install dependencies
run: pnpm install
- name: ✍️ Set up Windows code signing
- name: Set up Windows code signing
if: startsWith(matrix.platform, 'windows')
shell: bash
run: |
@@ -102,8 +117,8 @@ jobs:
dasel delete -f apps/app/tauri-release.conf.json 'bundle.windows.signCommand'
fi
- name: 🔨 Build macOS app
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
- name: Build macOS app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
if: startsWith(matrix.platform, 'macos')
env:
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
@@ -116,19 +131,19 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: 🔨 Build Linux app
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
- name: Build Linux app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
if: startsWith(matrix.platform, 'ubuntu')
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: 🔨 Build Windows app
- name: Build Windows app
run: |
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
if: startsWith(matrix.platform, 'windows')
env:
@@ -138,7 +153,7 @@ jobs:
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
- name: 📤 Upload app bundles
- name: Upload app bundles
uses: actions/upload-artifact@v4
with:
name: App bundle (${{ matrix.artifact-target-name }})
@@ -146,7 +161,13 @@ jobs:
target/release/bundle/appimage/Modrinth App_*.AppImage*
target/release/bundle/deb/Modrinth App_*.deb*
target/release/bundle/rpm/Modrinth App-*.rpm*
target/release/bundle/appimage/Modrinth App (dev-*)_*.AppImage*
target/release/bundle/deb/Modrinth App (dev-*)_*.deb*
target/release/bundle/rpm/Modrinth App (dev-*)-*.rpm*
target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz*
target/universal-apple-darwin/release/bundle/macos/Modrinth App (dev-*)*.app.tar.gz
target/universal-apple-darwin/release/bundle/dmg/Modrinth App (dev-*)*.dmg
target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg*
target/release/bundle/nsis/Modrinth App_*-setup.exe*
target/release/bundle/nsis/Modrinth App (dev-*)_*-setup.exe*
target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip*
+1
View File
@@ -65,6 +65,7 @@ app-playground-data/*
.astro
.claude
.letta
# labrinth demo fixtures
apps/labrinth/fixtures/demo
Generated
+598 -12
View File
@@ -521,6 +521,22 @@ dependencies = [
"zbus",
]
[[package]]
name = "astral-tokio-tar"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5"
dependencies = [
"filetime",
"futures-core",
"libc",
"portable-atomic",
"rustc-hash",
"tokio",
"tokio-stream",
"xattr",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -671,6 +687,28 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "async-stripe"
version = "0.41.0"
@@ -892,6 +930,49 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "axum"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"bytes",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"sync_wrapper",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.76"
@@ -1066,6 +1147,83 @@ dependencies = [
"piper",
]
[[package]]
name = "bollard"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b"
dependencies = [
"async-stream",
"base64 0.22.1",
"bitflags 2.9.4",
"bollard-buildkit-proto",
"bollard-stubs",
"bytes",
"chrono",
"futures-core",
"futures-util",
"hex",
"home",
"http 1.3.1",
"http-body-util",
"hyper 1.7.0",
"hyper-named-pipe",
"hyper-rustls 0.27.7",
"hyper-util",
"hyperlocal",
"log",
"num",
"pin-project-lite",
"rand 0.9.2",
"rustls 0.23.32",
"rustls-native-certs 0.8.1",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"serde",
"serde_derive",
"serde_json",
"serde_repr",
"serde_urlencoded",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-util",
"tonic 0.14.2",
"tower-service",
"url",
"winapi",
]
[[package]]
name = "bollard-buildkit-proto"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad"
dependencies = [
"prost 0.14.3",
"prost-types",
"tonic 0.14.2",
"tonic-prost",
"ureq",
]
[[package]]
name = "bollard-stubs"
version = "1.49.1-rc.28.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623"
dependencies = [
"base64 0.22.1",
"bollard-buildkit-proto",
"bytes",
"chrono",
"prost 0.14.3",
"serde",
"serde_json",
"serde_repr",
"serde_with",
]
[[package]]
name = "borsh"
version = "1.5.7"
@@ -2089,30 +2247,28 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "deadpool"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde"
dependencies = [
"deadpool-runtime",
"lazy_static",
"num_cpus",
"tokio",
"tracing",
]
[[package]]
name = "deadpool-redis"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0965b977f1244bc3783bb27cd79cfcff335a8341da18f79232d00504b18eb1a"
version = "0.22.1"
source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde"
dependencies = [
"deadpool",
"redis",
"tracing",
]
[[package]]
name = "deadpool-runtime"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
version = "0.1.5"
source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde"
dependencies = [
"tokio",
]
@@ -2378,6 +2534,17 @@ dependencies = [
"const-random",
]
[[package]]
name = "docker_credential"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8"
dependencies = [
"base64 0.21.7",
"serde",
"serde_json",
]
[[package]]
name = "document-features"
version = "0.2.11"
@@ -2679,6 +2846,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "etcetera"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.59.0",
]
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -3801,6 +3979,21 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-named-pipe"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
dependencies = [
"hex",
"hyper 1.7.0",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
"winapi",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
@@ -3835,6 +4028,19 @@ dependencies = [
"webpki-roots 1.0.3",
]
[[package]]
name = "hyper-timeout"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper 1.7.0",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
@@ -3877,6 +4083,21 @@ dependencies = [
"windows-registry",
]
[[package]]
name = "hyperlocal"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
dependencies = [
"hex",
"http-body-util",
"hyper 1.7.0",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
@@ -4566,6 +4787,7 @@ dependencies = [
"sha2",
"spdx",
"sqlx",
"sqlx-tracing",
"strum",
"thiserror 2.0.17",
"tikv-jemalloc-ctl",
@@ -4888,6 +5110,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "maxminddb"
version = "0.26.0"
@@ -5873,6 +6101,124 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "opentelemetry"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6"
dependencies = [
"futures-core",
"futures-sink",
"js-sys",
"pin-project-lite",
"thiserror 2.0.17",
"tracing",
]
[[package]]
name = "opentelemetry-appender-tracing"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e68f63eca5fad47e570e00e893094fc17be959c80c79a7d6ec1abdd5ae6ffc16"
dependencies = [
"opentelemetry",
"tracing",
"tracing-core",
"tracing-log",
"tracing-opentelemetry",
"tracing-subscriber",
]
[[package]]
name = "opentelemetry-http"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d"
dependencies = [
"async-trait",
"bytes",
"http 1.3.1",
"opentelemetry",
"reqwest",
]
[[package]]
name = "opentelemetry-otlp"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b"
dependencies = [
"http 1.3.1",
"opentelemetry",
"opentelemetry-http",
"opentelemetry-proto",
"opentelemetry_sdk",
"prost 0.13.5",
"reqwest",
"thiserror 2.0.17",
"tokio",
"tonic 0.13.1",
"tracing",
]
[[package]]
name = "opentelemetry-proto"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc"
dependencies = [
"opentelemetry",
"opentelemetry_sdk",
"prost 0.13.5",
"tonic 0.13.1",
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2"
[[package]]
name = "opentelemetry-testing"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562407eb254fea88076c8a410a733a8ee2075fd9c7fbde3da2a03d29e2cef104"
dependencies = [
"anyhow",
"opentelemetry",
"opentelemetry-appender-tracing",
"opentelemetry-otlp",
"opentelemetry-semantic-conventions",
"opentelemetry_sdk",
"serde",
"serde_json",
"serial_test",
"tempfile",
"testcontainers",
"tokio",
"tracing-opentelemetry",
"tracing-subscriber",
]
[[package]]
name = "opentelemetry_sdk"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b"
dependencies = [
"futures-channel",
"futures-executor",
"futures-util",
"opentelemetry",
"percent-encoding",
"rand 0.9.2",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -5997,6 +6343,31 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "parse-display"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
dependencies = [
"parse-display-derive",
"regex",
"regex-syntax",
]
[[package]]
name = "parse-display-derive"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
dependencies = [
"proc-macro2",
"quote",
"regex",
"regex-syntax",
"structmeta",
"syn 2.0.106",
]
[[package]]
name = "parse-size"
version = "1.1.0"
@@ -6384,7 +6755,7 @@ dependencies = [
"inferno",
"num",
"paste",
"prost",
"prost 0.13.5",
]
[[package]]
@@ -6590,7 +6961,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
"prost-derive",
"prost-derive 0.13.5",
]
[[package]]
name = "prost"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
dependencies = [
"bytes",
"prost-derive 0.14.3",
]
[[package]]
@@ -6606,6 +6987,28 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "prost-derive"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "prost-types"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
dependencies = [
"prost 0.14.3",
]
[[package]]
name = "protobuf"
version = "3.7.2"
@@ -7633,6 +8036,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scc"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
dependencies = [
"sdd",
]
[[package]]
name = "schannel"
version = "0.1.28"
@@ -7724,6 +8136,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "sdd"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
[[package]]
name = "seahash"
version = "4.1.0"
@@ -8170,6 +8588,32 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "serial_test"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555"
dependencies = [
"futures-executor",
"futures-util",
"log",
"once_cell",
"parking_lot",
"scc",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "serialize-to-javascript"
version = "0.1.2"
@@ -8576,7 +9020,7 @@ dependencies = [
"chrono",
"crc",
"dotenvy",
"etcetera",
"etcetera 0.8.0",
"futures-channel",
"futures-core",
"futures-util",
@@ -8629,6 +9073,20 @@ dependencies = [
"uuid 1.18.1",
]
[[package]]
name = "sqlx-tracing"
version = "0.2.0"
dependencies = [
"derive_more 2.0.1",
"futures",
"opentelemetry",
"opentelemetry-testing",
"sqlx",
"testcontainers",
"tokio",
"tracing",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -9405,6 +9863,35 @@ dependencies = [
"winapi",
]
[[package]]
name = "testcontainers"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3ac71069f20ecfa60c396316c283fbf35e6833a53dff551a31b5458da05edc"
dependencies = [
"astral-tokio-tar",
"async-trait",
"bollard",
"bytes",
"docker_credential",
"either",
"etcetera 0.10.0",
"futures",
"log",
"memchr",
"parse-display",
"pin-project-lite",
"serde",
"serde_json",
"serde_with",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-util",
"ulid",
"url",
]
[[package]]
name = "theseus"
version = "1.0.0-local"
@@ -9442,6 +9929,7 @@ dependencies = [
"notify",
"notify-debouncer-mini",
"p256",
"parking_lot",
"paste",
"path-util",
"phf 0.13.1",
@@ -9870,6 +10358,72 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "tonic"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9"
dependencies = [
"async-trait",
"base64 0.22.1",
"bytes",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.7.0",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"prost 0.13.5",
"tokio",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
dependencies = [
"async-trait",
"axum",
"base64 0.22.1",
"bytes",
"h2 0.4.12",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.7.0",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"socket2 0.6.1",
"sync_wrapper",
"tokio",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic-prost"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67"
dependencies = [
"bytes",
"prost 0.14.3",
"tonic 0.14.2",
]
[[package]]
name = "totp-rs"
version = "5.7.0"
@@ -9892,11 +10446,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.11.4",
"pin-project-lite",
"slab",
"sync_wrapper",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -10012,6 +10570,24 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-opentelemetry"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c"
dependencies = [
"js-sys",
"once_cell",
"opentelemetry",
"opentelemetry_sdk",
"smallvec",
"tracing",
"tracing-core",
"tracing-log",
"tracing-subscriber",
"web-time",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
@@ -10107,6 +10683,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "ulid"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
dependencies = [
"rand 0.9.2",
"web-time",
]
[[package]]
name = "uname"
version = "0.1.1"
+4 -1
View File
@@ -60,7 +60,7 @@ const_format = "0.2.34"
daedalus = { path = "packages/daedalus" }
dashmap = "6.1.0"
data-url = "0.3.2"
deadpool-redis = "0.22.0"
deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "db5fb00b036ecc8fe5f18853c559b745ffe47bde", version = "0.22.1" }
derive_more = "2.0.1"
directories = "6.0.0"
dirs = "6.0.0"
@@ -116,6 +116,7 @@ native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
notify-debouncer-mini = { version = "0.7.0", default-features = false }
p256 = "0.13.2"
parking_lot = "0.12.5"
paste = "1.0.15"
path-util = { path = "packages/path-util" }
phf = { version = "0.13.1", features = ["macros"] }
@@ -163,6 +164,7 @@ sha2 = "0.10.9"
shlex = "1.3.0"
spdx = "0.12.0"
sqlx = { version = "0.8.6", default-features = false }
sqlx-tracing = { path = "packages/sqlx-tracing" }
strum = "0.27.2"
sysinfo = { version = "0.37.2", default-features = false }
tar = "0.4.44"
@@ -268,4 +270,5 @@ codegen-units = 1 # Compile crates one after another so the compiler can optimi
# Specific profile for labrinth production builds
[profile.release-labrinth]
inherits = "release"
strip = false # Keep debug symbols for Sentry
panic = "unwind" # Don't exit the whole app on panic in production
+4
View File
@@ -17,3 +17,7 @@ extend-exclude = [
tou = "tou"
# Google Ad Manager
gam = "gam"
# short for "constants"
consts = "consts"
# short for "Copy"
Cpy = "Cpy"
+9 -3
View File
@@ -71,6 +71,7 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import UpdateAvailableToast from '@/components/ui/UpdateAvailableToast.vue'
import UpdateToast from '@/components/ui/UpdateToast.vue'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
@@ -143,6 +144,7 @@ const showOnboarding = ref(false)
const nativeDecorations = ref(false)
const os = ref('')
const isDevEnvironment = ref(false)
const stateInitialized = ref(false)
@@ -247,6 +249,7 @@ async function setupApp() {
os.value = await getOS()
const dev = await isDev()
isDevEnvironment.value = dev
const version = await getVersion()
showOnboarding.value = !onboarded
@@ -505,21 +508,23 @@ const restarting = ref(false)
const updateToastDismissed = ref(false)
const availableUpdate = ref(null)
const updateSize = ref(null)
const updatesEnabled = ref(true)
async function checkUpdates() {
if (!(await areUpdatesEnabled())) {
console.log('Skipping update check as updates are disabled in this build or environment')
updatesEnabled.value = false
return
}
async function performCheck() {
const update = await invoke('plugin:updater|check')
const isExistingUpdate = update.version === availableUpdate.value?.version
if (!update) {
console.log('No update available')
return
}
const isExistingUpdate = update.version === availableUpdate.value?.version
if (isExistingUpdate) {
console.log('Update is already known')
return
@@ -769,6 +774,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@restart="installUpdate"
@download="downloadAvailableUpdate"
/>
<UpdateAvailableToast v-else-if="!updatesEnabled && os === 'Linux' && !isDevEnvironment" />
</Transition>
</Suspense>
<Transition name="fade">
@@ -996,7 +1002,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<transition name="popup-survey">
<div
v-if="availableSurvey"
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-divider border-[1px] border-solid border-b-0 p-4"
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-surface-5 border-[1px] border-solid border-b-0 p-4"
>
<h2 class="text-lg font-extrabold mt-0 mb-2">Hey there Modrinth user!</h2>
<p class="m-0 leading-tight">
@@ -10,8 +10,13 @@ import {
TrashIcon,
XIcon,
} from '@modrinth/assets'
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
import { formatCategoryHeader } from '@modrinth/utils'
import {
Button,
DropdownSelect,
formatLoader,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
@@ -23,6 +28,8 @@ import { duplicate, remove } from '@/helpers/profile.js'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const props = defineProps({
instances: {
type: Array,
@@ -175,7 +182,7 @@ const filteredResults = computed(() => {
if (group === 'Loader') {
instances.forEach((instance) => {
const loader = formatCategoryHeader(instance.loader)
const loader = formatLoader(formatMessage, instance.loader)
if (!instanceMap.has(loader)) {
instanceMap.set(loader, [])
}
@@ -18,8 +18,8 @@ import { useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import Instance from '@/components/ui/Instance.vue'
import LegacyProjectCard from '@/components/ui/LegacyProjectCard.vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import ProjectCard from '@/components/ui/ProjectCard.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_by_profile_path } from '@/helpers/process.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
@@ -270,7 +270,7 @@ onUnmounted(() => {
/>
</section>
<section v-else ref="modsRow" class="projects">
<ProjectCard
<LegacyProjectCard
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
:key="project?.project_id"
ref="instanceComponents"
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils'
import { Avatar, ButtonStyled, FormattedTag } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
type Instance = {
@@ -37,7 +36,8 @@ defineProps<{
</span>
<span class="text-secondary flex items-center gap-2 font-semibold">
<GameIcon class="h-5 w-5 text-secondary" />
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
<FormattedTag :tag="instance.loader" enforce-type="loader" />
{{ instance.game_version }}
</span>
</span>
</span>
@@ -1,7 +1,7 @@
<script setup>
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
import { Avatar, TagItem } from '@modrinth/ui'
import { formatCategory, formatNumber } from '@modrinth/utils'
import { Avatar, FormattedTag, TagItem } from '@modrinth/ui'
import { formatNumber } from '@modrinth/utils'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { computed } from 'vue'
@@ -107,7 +107,7 @@ const toTransparent = computed(() => {
<div class="flex items-center gap-1 pr-2">
<TagIcon />
<TagItem>
{{ formatCategory(featuredCategory) }}
<FormattedTag :tag="featuredCategory" />
</TagItem>
</div>
</div>
@@ -1,7 +1,7 @@
<template>
<div
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
@click="
<ProjectCard
:title="project.title"
:link="
() => {
emit('open')
$router.push({
@@ -10,116 +10,58 @@
})
}
"
:author="{ name: project.author, link: `https://modrinth.com/user/${project.author}` }"
:icon-url="project.icon_url"
:summary="project.description"
:tags="project.display_categories"
:all-tags="project.categories"
:downloads="project.downloads"
:followers="project.follows"
:date-updated="project.date_modified"
:banner="project.featured_gallery ?? undefined"
:color="project.color ?? undefined"
:environment="
projectType
? ['mod', 'modpack'].includes(projectType)
? {
clientSide: project.client_side,
serverSide: project.server_side,
}
: undefined
: undefined
"
layout="list"
>
<div class="icon w-[96px] h-[96px] relative">
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
</div>
<div class="flex flex-col gap-2 overflow-hidden">
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
{{ project.title }}
</span>
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
</div>
<div class="m-0 line-clamp-2">
{{ project.description }}
</div>
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
<TagsIcon class="h-4 w-4 shrink-0" />
<div
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
<template #actions>
<ButtonStyled color="brand" type="outlined">
<button
:disabled="installed || installing"
class="shrink-0 no-wrap"
@click.stop="install()"
>
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
Client or server
<template v-if="!installed">
<DownloadIcon v-if="modpack || instance" />
<PlusIcon v-else />
</template>
<template
v-else-if="
(project.client_side === 'optional' || project.client_side === 'required') &&
(project.server_side === 'optional' || project.server_side === 'unsupported')
"
>
Client
</template>
<template
v-else-if="
(project.server_side === 'optional' || project.server_side === 'required') &&
(project.client_side === 'optional' || project.client_side === 'unsupported')
"
>
Server
</template>
<template
v-else-if="
project.client_side === 'unsupported' && project.server_side === 'unsupported'
"
>
Unsupported
</template>
<template
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
>
Client and server
</template>
</div>
<div
v-for="tag in categories"
:key="tag"
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
>
{{ formatCategory(tag.name) }}
</div>
</div>
</div>
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
<div class="flex items-center gap-2">
<DownloadIcon class="shrink-0" />
<span>
{{ formatNumber(project.downloads) }}
<span class="text-secondary">downloads</span>
</span>
</div>
<div class="flex items-center gap-2">
<HeartIcon class="shrink-0" />
<span>
{{ formatNumber(project.follows ?? project.followers) }}
<span class="text-secondary">followers</span>
</span>
</div>
<div class="mt-auto relative">
<div class="absolute bottom-0 right-0 w-fit">
<ButtonStyled color="brand" type="outlined">
<button
:disabled="installed || installing"
class="shrink-0 no-wrap"
@click.stop="install()"
>
<template v-if="!installed">
<DownloadIcon v-if="modpack || instance" />
<PlusIcon v-else />
</template>
<CheckIcon v-else />
{{
installing
? 'Installing'
: installed
? 'Installed'
: modpack || instance
? 'Install'
: 'Add to an instance'
}}
</button>
</ButtonStyled>
</div>
</div>
</div>
</div>
<CheckIcon v-else />
{{
installing
? 'Installing'
: installed
? 'Installed'
: modpack || instance
? 'Install'
: 'Add to an instance'
}}
</button>
</ButtonStyled>
</template>
</ProjectCard>
</template>
<script setup>
import { CheckIcon, DownloadIcon, HeartIcon, PlusIcon, TagsIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { formatCategory, formatNumber } from '@modrinth/utils'
import { CheckIcon, DownloadIcon, PlusIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { computed, ref } from 'vue'
@@ -141,10 +83,6 @@ const props = defineProps({
type: Object,
required: true,
},
categories: {
type: Array,
required: true,
},
instance: {
type: Object,
default: null,
@@ -157,6 +95,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
projectType: {
type: String,
default: undefined,
},
})
const emit = defineEmits(['open', 'install'])
@@ -0,0 +1,84 @@
<script setup lang="ts">
import { XIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages, defineMessages, useVIntl } from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app'
import { onMounted, onUnmounted, ref } from 'vue'
const { formatMessage } = useVIntl()
const dismissed = ref(false)
const availableUpdate = ref<{ version: string } | null>(null)
let checkInterval: ReturnType<typeof setInterval> | null = null
async function checkForUpdate() {
try {
const [response, currentVersion] = await Promise.all([
fetch('https://launcher-files.modrinth.com/updates.json'),
getVersion(),
])
const updates = await response.json()
const latestVersion = updates?.version
if (latestVersion && latestVersion !== currentVersion) {
if (latestVersion !== availableUpdate.value?.version) {
availableUpdate.value = { version: latestVersion }
dismissed.value = false
}
}
} catch (e) {
console.error('Failed to check for updates:', e)
}
}
function dismiss() {
dismissed.value = true
}
onMounted(() => {
checkForUpdate()
checkInterval = setInterval(checkForUpdate, 5 * 60 * 1000)
})
onUnmounted(() => {
if (checkInterval) {
clearInterval(checkInterval)
}
})
const messages = defineMessages({
title: {
id: 'app.update-toast.title',
defaultMessage: 'Update available',
},
body: {
id: 'app.update-toast.body.linux',
defaultMessage:
'Modrinth App v{version} is available. Use your package manager to update for the latest features and fixes!',
},
download: {
id: 'app.update-toast.download-page',
defaultMessage: 'Download',
},
})
</script>
<template>
<div
v-if="availableUpdate && !dismissed"
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-surface-5 border-solid border-[2px]"
>
<div class="flex min-w-[25rem] gap-4">
<h2 class="whitespace-nowrap text-base text-contrast font-semibold m-0 grow">
{{ formatMessage(messages.title) }}
</h2>
<ButtonStyled size="small" circular>
<button v-tooltip="formatMessage(commonMessages.closeButton)" @click="dismiss">
<XIcon />
</button>
</ButtonStyled>
</div>
<p class="text-sm mt-2 mb-0">
{{ formatMessage(messages.body, { version: availableUpdate.version }) }}
</p>
</div>
</template>
@@ -68,7 +68,7 @@ const messages = defineMessages({
</script>
<template>
<div
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-divider border-solid border-[2px]"
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-surface-5 border-solid border-[2px]"
:class="{
'download-complete': progress === 1,
}"
@@ -28,7 +28,7 @@
:custom-label="
(version) =>
`${version?.name} (${version?.loaders
.map((name) => formatCategory(name))
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version?.game_versions.join(', ')})`
"
:max-height="150"
@@ -36,7 +36,9 @@
<span v-else>
<span>
{{ selectedVersion?.name }} ({{
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
selectedVersion?.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')
}}
- {{ selectedVersion?.game_versions.join(', ') }})
</span>
@@ -57,8 +59,7 @@
<script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils'
import { Button, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { ref } from 'vue'
import Multiselect from 'vue-multiselect'
@@ -67,6 +68,7 @@ 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)
@@ -1,6 +1,7 @@
<script setup lang="ts">
import {
DownloadIcon,
getLoaderIcon,
HammerIcon,
IssuesIcon,
SpinnerIcon,
@@ -17,16 +18,11 @@ import {
Chips,
Combobox,
defineMessages,
formatLoader,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import {
formatCategory,
type GameVersionTag,
type PlatformTag,
type Project,
type Version,
} from '@modrinth/utils'
import type { GameVersionTag, PlatformTag, Project, Version } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
@@ -129,9 +125,7 @@ if (props.instance.linked_data && props.instance.linked_data.project_id && !prop
fetching.value = false
}
const currentLoaderIcon = computed(
() => loaders?.value.find((x) => x.name === props.instance.loader)?.icon,
)
const currentLoaderIcon = computed(() => getLoaderIcon(props.instance.loader))
const gameVersionsForLoader = computed(() => {
return all_game_versions?.value.filter((item) => {
@@ -550,7 +544,7 @@ const messages = defineMessages({
v-else
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 [&_svg]:h-full [&_svg]:w-full"
>
<div v-if="!!currentLoaderIcon" class="contents" v-html="currentLoaderIcon" />
<component :is="currentLoaderIcon" v-if="currentLoaderIcon" />
<WrenchIcon v-else />
</div>
<div class="flex flex-col gap-2 justify-center">
@@ -569,7 +563,7 @@ const messages = defineMessages({
? modpackVersion
? modpackVersion?.version_number
: 'Unknown version'
: formatCategory(instance.loader)
: formatLoader(formatMessage, instance.loader)
}}
<template v-if="instance.loader !== 'vanilla' && !modpackProject">
{{ instance.loader_version || formatMessage(messages.unknownVersion) }}
@@ -677,7 +671,11 @@ const messages = defineMessages({
</div>
<template v-if="loader !== 'vanilla'">
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
{{
formatMessage(messages.loaderVersion, {
loader: formatLoader(formatMessage, loader),
})
}}
</h2>
<Combobox
v-if="selectableLoaderVersions"
@@ -709,7 +707,7 @@ const messages = defineMessages({
? messages.alreadyInstalledVanilla
: messages.alreadyInstalledModded,
{
platform: formatCategory(loader),
platform: formatLoader(formatMessage, loader),
version: instance.loader_version,
game_version: gameVersion,
},
@@ -0,0 +1,189 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.installToPlay)" :closable="true">
<div class="flex flex-col gap-6 max-w-[500px]">
<Admonition type="info" :header="formatMessage(messages.sharedServerInstance)">
{{ formatMessage(messages.serverRequiresMods) }}
</Admonition>
<div v-if="sharedBy?.name" class="flex items-center gap-2 text-sm text-secondary">
<Avatar
v-if="sharedBy?.icon_url"
:src="sharedBy.icon_url"
:alt="sharedBy.name"
size="24px"
/>
<span>
<IntlFormatted :message-id="messages.sharedByToday">
<template #~name>
<span class="font-semibold text-contrast">{{ sharedBy.name }}</span>
</template>
</IntlFormatted>
</span>
</div>
<div class="flex flex-col gap-2">
<span class="font-semibold text-contrast">
{{ formatMessage(messages.sharedInstance) }}
</span>
<div class="flex items-center gap-3 rounded-xl bg-surface-4 p-3">
<Avatar :src="project.icon_url" :alt="project.title" size="48px" />
<div class="flex flex-col gap-0.5">
<span class="font-semibold text-contrast">{{ project.title }}</span>
<span class="text-sm text-secondary">
{{ loaderDisplay }} {{ project.game_versions?.[0] }}
<template v-if="modCount">
· {{ formatMessage(messages.modCount, { count: modCount }) }}
</template>
</span>
</div>
</div>
</div>
</div>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled>
<button @click="handleDecline">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button @click="handleAccept">
<DownloadIcon />
{{ formatMessage(messages.installButton) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { DownloadIcon, XIcon } from '@modrinth/assets'
import {
Admonition,
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
formatLoader,
IntlFormatted,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { get_organization, get_team, get_version } from '@/helpers/cache.js'
import { install } from '@/store/install.js'
const props = defineProps<{
project: Labrinth.Projects.v2.Project
}>()
const modal = ref<InstanceType<typeof NewModal>>()
const { formatMessage } = useVIntl()
const { data: organization } = useQuery({
queryKey: computed(() => ['organization', props.project.organization]),
queryFn: () => get_organization(props.project.organization!, 'must_revalidate'),
enabled: computed(() => !!props.project.organization),
})
const { data: teamMembers } = useQuery({
queryKey: computed(() => ['team', props.project.team]),
queryFn: () => get_team(props.project.team, 'must_revalidate'),
enabled: computed(() => !!props.project.team && !props.project.organization),
})
const sharedBy = computed(() => {
if (organization.value) {
return {
name: organization.value.name,
icon_url: organization.value.icon_url,
}
}
if (teamMembers.value) {
const owner = teamMembers.value.find((member: { is_owner: boolean }) => member.is_owner)
if (owner) {
return {
name: owner.user.username,
icon_url: owner.user.avatar_url,
}
}
}
return null
})
const loaderDisplay = computed(() => {
const loader = props.project.loaders?.[0]
if (!loader) return ''
return formatLoader(formatMessage, loader)
})
// Fetch the most recent version to get mod count from dependencies
const latestVersionId = computed(() => props.project.versions?.[0] ?? null)
const { data: latestVersion } = useQuery({
queryKey: computed(() => ['version', latestVersionId.value]),
queryFn: () => get_version(latestVersionId.value, 'must_revalidate'),
enabled: computed(() => !!latestVersionId.value),
})
const modCount = computed(() => latestVersion.value?.dependencies?.length)
async function handleAccept() {
hide()
try {
await install(props.project.id, null, null, 'ProjectPageInstallToPlayModal')
} catch (error) {
console.error('Failed to install project from InstallToPlayModal:', error)
}
}
function handleDecline() {
hide()
}
function show(e?: MouseEvent) {
modal.value?.show(e)
}
function hide() {
modal.value?.hide()
}
const messages = defineMessages({
installToPlay: {
id: 'app.modal.install-to-play.header',
defaultMessage: 'Install to play',
},
sharedServerInstance: {
id: 'app.modal.install-to-play.shared-server-instance',
defaultMessage: 'Shared server instance',
},
serverRequiresMods: {
id: 'app.modal.install-to-play.server-requires-mods',
defaultMessage:
'This server requires mods to play. Click install to set up the required files from Modrinth.',
},
sharedByToday: {
id: 'app.modal.install-to-play.shared-by-today',
defaultMessage: '{name} shared this instance with you today.',
},
sharedInstance: {
id: 'app.modal.install-to-play.shared-instance',
defaultMessage: 'Shared instance',
},
modCount: {
id: 'app.modal.install-to-play.mod-count',
defaultMessage: '{count, plural, one {# mod} other {# mods}}',
},
installButton: {
id: 'app.modal.install-to-play.install-button',
defaultMessage: 'Install',
},
})
defineExpose({ show, hide })
</script>
@@ -7,7 +7,14 @@ import {
MonitorIcon,
WrenchIcon,
} from '@modrinth/assets'
import { Avatar, defineMessage, TabbedModal, type TabbedModalTab, useVIntl } from '@modrinth/ui'
import {
Avatar,
commonMessages,
defineMessage,
TabbedModal,
type TabbedModalTab,
useVIntl,
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { ref } from 'vue'
@@ -74,11 +81,6 @@ function show() {
}
defineExpose({ show })
const titleMessage = defineMessage({
id: 'instance.settings.title',
defaultMessage: 'Settings',
})
</script>
<template>
<ModalWrapper ref="modal">
@@ -90,7 +92,9 @@ const titleMessage = defineMessage({
:tint-by="props.instance.path"
/>
{{ instance.name }} <ChevronRightIcon />
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
<span class="font-extrabold text-contrast">{{
formatMessage(commonMessages.settingsLabel)
}}</span>
</span>
</template>
@@ -0,0 +1,382 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.updateToPlay)" :closable="true" no-padding>
<div class="max-w-[500px]">
<div class="flex flex-col gap-4 p-4">
<Admonition type="warning" :header="formatMessage(messages.updateRequired)">
{{ formatMessage(messages.updateRequiredDescription, { name: instance.name }) }}
</Admonition>
<div v-if="diffs.length" class="flex flex-col gap-2">
<span v-if="publishedDate" class="text-contrast font-semibold">{{
formatMessage(messages.publishedDate, { date: publishedDate })
}}</span>
<div class="flex gap-2">
<div v-if="removedCount" class="flex gap-1 items-center">
<MinusIcon />
{{ formatMessage(messages.removedCount, { count: removedCount }) }}
</div>
<div v-if="addedCount" class="flex gap-1 items-center">
<PlusIcon />
{{ formatMessage(messages.addedCount, { count: addedCount }) }}
</div>
<div v-if="updatedCount" class="flex gap-1 items-center">
<RefreshCwIcon />
{{ formatMessage(messages.updatedCount, { count: updatedCount }) }}
</div>
</div>
</div>
</div>
<div v-if="diffs.length" class="flex flex-col bg-surface-2 p-4 max-h-[272px] overflow-y-auto">
<div
v-for="diff in diffs"
:key="diff.project_id"
class="grid grid-cols-[auto_1fr_1fr_1fr] items-center min-h-10 h-10 gap-2"
>
<div class="flex flex-col justify-between items-center">
<div class="w-[1px] h-2"></div>
<PlusIcon v-if="diff.type === 'added'" />
<MinusIcon v-else-if="diff.type === 'removed'" />
<RefreshCwIcon v-else />
<div class="bg-surface-5 w-[1px] h-2 relative top-1"></div>
</div>
<div class="flex gap-1 col-span-2">
<span class="text-sm">{{ formatMessage(diffTypeMessages[diff.type]) }}</span>
<span
v-if="diff.project"
v-tooltip="diff.project.title"
class="text-sm text-contrast font-medium truncate"
>
{{ diff.project.title }}
</span>
</div>
<span
v-if="getFilename(diff.newVersion) || getFilename(diff.currentVersion)"
v-tooltip="getFilename(diff.newVersion) || getFilename(diff.currentVersion)"
class="text-xs truncate text-right"
>
{{ getFilename(diff.newVersion) || getFilename(diff.currentVersion) }}
</span>
</div>
</div>
</div>
<template #actions>
<div class="flex justify-between gap-2">
<ButtonStyled color="red" type="transparent">
<button @click="handleReport">
<ReportIcon />
{{ formatMessage(commonMessages.reportButton) }}
</button>
</ButtonStyled>
<div class="flex gap-2">
<ButtonStyled>
<button @click="handleDecline">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button @click="handleUpdate">
<DownloadIcon />
{{ formatMessage(commonMessages.updateButton) }}
</button>
</ButtonStyled>
</div>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
DownloadIcon,
MinusIcon,
PlusIcon,
RefreshCwIcon,
ReportIcon,
XIcon,
} from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
commonMessages,
defineMessages,
NewModal,
useVIntl,
} from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import dayjs from 'dayjs'
import { computed, ref, watch } from 'vue'
import { get_project, get_project_many, get_version_many } from '@/helpers/cache.js'
import { update_managed_modrinth_version } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
type Dependency = Labrinth.Versions.v3.Dependency
type Version = Labrinth.Versions.v2.Version
interface BaseDiff {
project_id: string
project?: {
title: string
icon_url?: string
slug: string
}
currentVersionId?: string
newVersionId?: string
currentVersion?: Version
newVersion?: Version
}
interface AddedDiff extends BaseDiff {
type: 'added'
newVersionId: string
}
interface RemovedDiff extends BaseDiff {
type: 'removed'
}
interface UpdatedDiff extends BaseDiff {
type: 'updated'
currentVersionId: string
newVersionId: string
}
type DependencyDiff = AddedDiff | RemovedDiff | UpdatedDiff
type ProjectInfo = {
id: string
title: string
icon_url?: string
slug: string
}
const { instance } = defineProps<{
instance: GameInstance
}>()
const { formatMessage } = useVIntl()
const modal = ref<InstanceType<typeof NewModal>>()
const diffs = ref<DependencyDiff[]>([])
const latestVersionId = ref<string | null>(null)
const latestVersion = ref<Version | null>(null)
const removedCount = computed(() => diffs.value.filter((d) => d.type === 'removed').length)
const addedCount = computed(() => diffs.value.filter((d) => d.type === 'added').length)
const updatedCount = computed(() => diffs.value.filter((d) => d.type === 'updated').length)
const publishedDate = computed(() =>
latestVersion.value?.date_published ? new Date(latestVersion.value.date_published) : null,
)
function getFilename(version?: Version): string | undefined {
return version?.files.find((f) => f.primary)?.filename
}
async function computeDependencyDiffs(
currentDeps: Dependency[],
latestDeps: Dependency[],
): Promise<DependencyDiff[]> {
const currentByProject = new Map<string, Dependency>(
currentDeps.map((d) => [d.project_id || '', d]),
)
const latestByProject = new Map<string, Dependency>(
latestDeps.map((d) => [d.project_id || '', d]),
)
const diffs: DependencyDiff[] = []
// Find added and updated dependencies
latestByProject.forEach((latestDep, projectId) => {
if (!projectId) return
const currentDep = currentByProject.get(projectId)
if (!currentDep && latestDep.version_id) {
diffs.push({ type: 'added', project_id: projectId, newVersionId: latestDep.version_id })
} else if (
currentDep?.version_id &&
latestDep?.version_id &&
currentDep?.version_id !== latestDep.version_id
) {
diffs.push({
type: 'updated',
project_id: projectId,
currentVersionId: currentDep.version_id,
newVersionId: latestDep.version_id,
})
}
})
// Find removed dependencies
currentByProject.forEach((currentDep, projectId) => {
if (!projectId) return
if (!latestByProject.has(projectId)) {
diffs.push({
type: 'removed',
project_id: projectId,
currentVersionId: currentDep.version_id,
})
}
})
// Fetch projects and versions of diffs
const allProjectIds = [...new Set(diffs.map((d) => d.project_id).filter(Boolean))]
const allVersionIds = [
...new Set(
[...diffs.map((d) => d.newVersionId), ...diffs.map((d) => d.currentVersionId)].filter(
Boolean,
),
),
] as string[]
const [projects, versions] = await Promise.all([
get_project_many(allProjectIds, 'must_revalidate'),
get_version_many(allVersionIds, 'must_revalidate'),
])
const projectMap = new Map<string, ProjectInfo>(projects.map((p: ProjectInfo) => [p.id, p]))
const versionMap = new Map<string, Version>(versions.map((v: Version) => [v.id, v]))
return diffs
.map((diff) => {
const project = projectMap.get(diff.project_id)
return {
...diff,
project: project
? { title: project.title, icon_url: project.icon_url, slug: project.slug }
: undefined,
currentVersion: diff.currentVersionId ? versionMap.get(diff.currentVersionId) : undefined,
newVersion: diff.newVersionId ? versionMap.get(diff.newVersionId) : undefined,
}
})
.sort((a, b) => {
const typeOrder = { removed: 0, added: 1, updated: 2 }
const typeCompare = typeOrder[a.type] - typeOrder[b.type]
if (typeCompare !== 0) return typeCompare
const aDate = a.newVersion?.date_published || a.currentVersion?.date_published || ''
const bDate = b.newVersion?.date_published || b.currentVersion?.date_published || ''
return dayjs(bDate).valueOf() - dayjs(aDate).valueOf()
})
}
async function checkUpdateAvailable(instance: GameInstance): Promise<DependencyDiff[] | null> {
if (!instance.linked_data) return null
try {
const project = await get_project(instance.linked_data.project_id, 'must_revalidate')
if (!project || !project.versions || project.versions.length === 0) {
return null
}
const versions = await get_version_many(project.versions, 'must_revalidate')
const sortedVersions = versions.sort(
(a: { date_published: string }, b: { date_published: string }) =>
dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf(),
)
latestVersion.value = sortedVersions[0]
latestVersionId.value = latestVersion.value?.id || null
const currentVersionId = instance.linked_data.version_id
const currentVersion = versions.find((v: { id: string }) => v.id === currentVersionId)
// Compute dependency diffs between current and latest version
if (currentVersion && latestVersion.value) {
return await computeDependencyDiffs(
currentVersion.dependencies || [],
latestVersion.value.dependencies || [],
)
}
} catch (error) {
console.error('Error checking for updates:', error)
return null
}
return null
}
watch(
() => instance,
async () => {
const result = await checkUpdateAvailable(instance)
diffs.value = result || []
},
{ immediate: true, deep: true },
)
async function handleUpdate() {
hide()
try {
if (latestVersionId.value) {
await update_managed_modrinth_version(instance.path, latestVersionId.value)
}
} catch (error) {
console.error('Error updating instance:', error)
}
}
function handleReport() {
if (instance.linked_data?.project_id) {
openUrl(`https://modrinth.com/report?item=project&itemID=${instance.linked_data.project_id}`)
}
}
function handleDecline() {
hide()
}
function show(e?: MouseEvent) {
modal.value?.show(e)
}
function hide() {
modal.value?.hide()
}
const messages = defineMessages({
updateToPlay: {
id: 'app.modal.update-to-play.header',
defaultMessage: 'Update to play',
},
updateRequired: {
id: 'app.modal.update-to-play.update-required',
defaultMessage: 'Update required',
},
updateRequiredDescription: {
id: 'app.modal.update-to-play.update-required-description',
defaultMessage:
'An update is required to play {name}. Please update to the latest version to launch the game.',
},
publishedDate: {
id: 'app.modal.update-to-play.published-date',
defaultMessage: '{date, date, long}',
},
removedCount: {
id: 'app.modal.update-to-play.removed-count',
defaultMessage: '{count} removed',
},
addedCount: {
id: 'app.modal.update-to-play.added-count',
defaultMessage: '{count} added',
},
updatedCount: {
id: 'app.modal.update-to-play.updated-count',
defaultMessage: '{count} updated',
},
})
const diffTypeMessages = defineMessages({
added: {
id: 'app.modal.update-to-play.diff-type.added',
defaultMessage: 'Added',
},
removed: {
id: 'app.modal.update-to-play.diff-type.removed',
defaultMessage: 'Removed',
},
updated: {
id: 'app.modal.update-to-play.diff-type.updated',
defaultMessage: 'Updated',
},
})
defineExpose({ show, hide })
</script>
@@ -33,7 +33,7 @@
"message": "إدارة الموارد"
},
"app.update-toast.body": {
"message": "تطبيق Modrinth الإصدار {version} جاهز للتثبيت! أعد التحميل لتحديث التطبيق الآن، أو سيتم التحديث تلقائيًا عند إغلاق تطبيق Modrinth."
"message": "تطبيق Modrinth الإصدار {version} جاهز للتثبيت!\nأعد التحميل لتحديث التطبيق الآن، أو سيتم التحديث تلقائيًا عند إغلاق تطبيق Modrinth."
},
"app.update-toast.body.download-complete": {
"message": "تطبيق Modrinth الإصدار {version} جاهز للتثبيت!\nأعد التحميل لتحديث التطبيق الآن، أو سيتم التحديث تلقائيًا عند إغلاق تطبيق Modrinth."
@@ -87,7 +87,7 @@
"message": "جار إضافة صديق"
},
"friends.add-friend.username.description": {
"message": "قد يختلف عن اسم المستخدم الخاص بهم في Minecraft!"
"message": "قد يختلف عن اسم المستخدم الخاص بهم في ماينكرافت!"
},
"friends.add-friend.username.placeholder": {
"message": "أدخل اسم مستخدم Modrinth..."
@@ -141,7 +141,7 @@
"message": "إضافة واللعب"
},
"instance.add-server.add-server": {
"message": "إضافة الخادم"
"message": "إضافة خادم"
},
"instance.add-server.resource-pack.disabled": {
"message": "معطَّل"
@@ -186,7 +186,7 @@
"message": "الاسم"
},
"instance.server-modal.placeholder-name": {
"message": "خادم ماين كرافت"
"message": "خادم ماينكرافت"
},
"instance.server-modal.resource-pack": {
"message": "حزمة الموارد"
@@ -279,7 +279,7 @@
"message": "مُغلِّف"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "أمر غلاف لتشغيل ماين كرافت."
"message": "أمر غلاف لتشغيل ماينكرافت."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "أدخل أمر الغلاف..."
@@ -288,7 +288,7 @@
"message": "تثبيت"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "{platform} {version} لماين كرافت {game_version} مثبت بالفعل"
"message": "{platform} {version} لماينكرافت {game_version} مثبت بالفعل"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "فانيلا {game_version} مُثبّتة بالفعل"
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "أدخل العرض..."
},
"instance.settings.title": {
"message": "الإعدادات"
},
"instance.worlds.a_minecraft_server": {
"message": "خادم ماين كرافت"
},
@@ -33,10 +33,13 @@
"message": "Správa zdrojů"
},
"app.update-toast.body": {
"message": "Aplikace Modrinth v{version} je připravena k instalaci! Naninstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
"message": "Aplikace Modrinth v{version} je připravena k instalaci! Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-toast.body.download-complete": {
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Naninstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} je k dispozici. Aktualizujte pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
},
"app.update-toast.body.metered": {
"message": "Aplikace Modrinth v{version} je nyní k dispozici! Protože jste v měřené síti, nebyla stažena automaticky."
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Stahování ({size})"
},
"app.update-toast.download-page": {
"message": "Stáhnout"
},
"app.update-toast.downloading": {
"message": "Stahování..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Zadejte šířku..."
},
"instance.settings.title": {
"message": "Nastavení"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft Server"
},
@@ -261,7 +261,7 @@
"message": "Kørte efter spillet lukkes."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Indskriv post-exit kommando..."
"message": "Indtast post-exit kommando..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Pre-launch"
@@ -270,7 +270,7 @@
"message": "Kørt før en instance bliver kørt."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Indskriv pre-launch kommando..."
"message": "Indtast pre-launch kommando..."
},
"instance.settings.tabs.hooks.title": {
"message": "Spille lunch hooks"
@@ -282,7 +282,7 @@
"message": "Wrapper kommando for at køre Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Indskriv wrapper kommando..."
"message": "Indtast wrapper kommando..."
},
"instance.settings.tabs.installation": {
"message": "Installation"
@@ -459,7 +459,7 @@
"message": "Højden af spillevinduet når kørt."
},
"instance.settings.tabs.window.height.enter": {
"message": "Indskriv højde..."
"message": "Indtast højde..."
},
"instance.settings.tabs.window.width": {
"message": "Bredde"
@@ -468,10 +468,7 @@
"message": "Bredden på spille vinduet når kørt."
},
"instance.settings.tabs.window.width.enter": {
"message": "Indskriv bredde..."
},
"instance.settings.title": {
"message": "Indstillinger"
"message": "Indtast bredde..."
},
"instance.worlds.a_minecraft_server": {
"message": "En Minecraft server"
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Lade die App neu, oder schliesse sie, um zu aktualisieren."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} ist verfügbar. Benutze deinen Paketmanager zum aktualisieren um die neusten Features und Bugfixes zu erhalten!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} ist jetzt verfügbar! Da du in einem begrenzten Netzwerk bist, haben wir es nicht automatisch heruntergeladen."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Herunterladen ({size})"
},
"app.update-toast.download-page": {
"message": "Herunterladen"
},
"app.update-toast.downloading": {
"message": "Lade herunter..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Breite eingeben..."
},
"instance.settings.title": {
"message": "Einstellungen"
},
"instance.worlds.a_minecraft_server": {
"message": "Ä Minecraft-Server"
},
@@ -33,10 +33,13 @@
"message": "Ressourcenmanagement"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Jetzt neu laden, um zu aktualisieren, oder automatisch beim Schließen der Modrinth App."
"message": "Modrinth App v{version} ist bereit zur Installation! Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} ist bereit zur Installation! Jetzt neu laden, um zu aktualisieren, oder automatisch beim Schließen der Modrinth App."
"message": "Modrinth App v{version} wurde heruntergeladen. Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} ist verfügbar. Verwende deinen Paketmanager, um die neuesten Funktionen und Fehlerbehebungen zu installieren!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} ist jetzt verfügbar! Da du ein getaktetes Netzwerk nutzt, haben wir den Download nicht automatisch gestartet."
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Herunterladen ({size})"
},
"app.update-toast.download-page": {
"message": "Herunterladen"
},
"app.update-toast.downloading": {
"message": "Wird Heruntergeladen..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Breite eingeben..."
},
"instance.settings.title": {
"message": "Einstellungen"
},
"instance.worlds.a_minecraft_server": {
"message": "Ein Minecraft-Server"
},
+57 -3
View File
@@ -5,6 +5,57 @@
"app.auth-servers.unreachable.header": {
"message": "Cannot reach authentication servers"
},
"app.modal.install-to-play.header": {
"message": "Install to play"
},
"app.modal.install-to-play.install-button": {
"message": "Install"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "This server requires mods to play. Click install to set up the required files from Modrinth."
},
"app.modal.install-to-play.shared-by-today": {
"message": "{name} shared this instance with you today."
},
"app.modal.install-to-play.shared-instance": {
"message": "Shared instance"
},
"app.modal.install-to-play.shared-server-instance": {
"message": "Shared server instance"
},
"app.modal.update-to-play.added-count": {
"message": "{count} added"
},
"app.modal.update-to-play.diff-type.added": {
"message": "Added"
},
"app.modal.update-to-play.diff-type.removed": {
"message": "Removed"
},
"app.modal.update-to-play.diff-type.updated": {
"message": "Updated"
},
"app.modal.update-to-play.header": {
"message": "Update to play"
},
"app.modal.update-to-play.published-date": {
"message": "{date, date, long}"
},
"app.modal.update-to-play.removed-count": {
"message": "{count} removed"
},
"app.modal.update-to-play.update-required": {
"message": "Update required"
},
"app.modal.update-to-play.update-required-description": {
"message": "An update is required to play {name}. Please update to the latest version to launch the game."
},
"app.modal.update-to-play.updated-count": {
"message": "{count} updated"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
@@ -38,6 +89,9 @@
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} is available. Use your package manager to update for the latest features and fixes!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it."
},
@@ -47,6 +101,9 @@
"app.update-toast.download": {
"message": "Download ({size})"
},
"app.update-toast.download-page": {
"message": "Download"
},
"app.update-toast.downloading": {
"message": "Downloading..."
},
@@ -470,9 +527,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Enter width..."
},
"instance.settings.title": {
"message": "Settings"
},
"instance.worlds.a_minecraft_server": {
"message": "A Minecraft Server"
},
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "La descarga de la Modrinth App v{version} ha finalizado. Actualiza ahora o automáticamente al cerrar la aplicación Modrinth."
},
"app.update-toast.body.linux": {
"message": "La aplicación Modrinth v{version} ya está disponible. ¡Utiliza tu gestor de paquetes para actualizarla y disfrutar de las últimas funciones y correcciones!"
},
"app.update-toast.body.metered": {
"message": "¡Modrinth App v{version} ya está disponible! Como estás en una red con límite de datos, no se descargó automáticamente."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Descargar ({size})"
},
"app.update-toast.download-page": {
"message": "Descargar"
},
"app.update-toast.downloading": {
"message": "Descargando..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Ingresa el ancho..."
},
"instance.settings.title": {
"message": "Configuración"
},
"instance.worlds.a_minecraft_server": {
"message": "Un servidor de Minecraft"
},
@@ -2,6 +2,9 @@
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft podrían estar inactivos. Comprueba tu conexión a internet e inténtalo más tarde."
},
"app.auth-servers.unreachable.header": {
"message": "No se puede conectar con los servidores de autenticación"
},
"app.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
@@ -35,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "La descarga de la versión v{version} de Modrinth ha finalizado. Actualice ahora o automáticamente al cerrar la aplicación."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} está disponible. ¡Usa tu gestor de paquetes para actualizar a la última versión con nuevas funciones y correcciones!"
},
"app.update-toast.body.metered": {
"message": "¡La versión v{version} de Modrinth App ya está disponible! Como estás conectado a una red con límite de datos, no la hemos descargado automáticamente."
},
@@ -44,6 +50,9 @@
"app.update-toast.download": {
"message": "Descarga ({size})"
},
"app.update-toast.download-page": {
"message": "Descargar"
},
"app.update-toast.downloading": {
"message": "Descargando..."
},
@@ -467,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Introduzca el ancho..."
},
"instance.settings.title": {
"message": "Ajustes"
},
"instance.worlds.a_minecraft_server": {
"message": "Un Servidor de Minecraft"
},
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Syötä leveys..."
},
"instance.settings.title": {
"message": "Asetukset"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft palvelin"
},
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Magagamit na ngayon ang Modrinth App v{version}. Gamitin ang iyong package manager upang i-update sa pinabagong tampok at pag-aayos!"
},
"app.update-toast.body.metered": {
"message": "Magagamit na ngayon ang Modrinth App v{version}! Hindi namin dinanload kaagad dahil naka-metro ang inyong network."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "I-download ({size})"
},
"app.update-toast.download-page": {
"message": "I-download"
},
"app.update-toast.downloading": {
"message": "Nagda-download..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Ilagay ang lapad..."
},
"instance.settings.title": {
"message": "Mga Setting"
},
"instance.worlds.a_minecraft_server": {
"message": "Isang Minecraft Server"
},
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "Le téléchargement de Modrinth App v{version} est terminé ! Relancez l'application pour mettre à jour maintenant, ou automatiquement à la fermeture de Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Modrith App v{version} est disponible. Utilisez votre gestionnaire de paquets pour obtenir les changements de la mise à jour !"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} est disponible dès maintenant ! Lorsque vous êtes sur un réseau limité ou en données mobiles, nous ne téléchargerons pas les mises à jour automatiquement."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Télécharger ({size})"
},
"app.update-toast.download-page": {
"message": "Télécharger"
},
"app.update-toast.downloading": {
"message": "Téléchargement..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Entrer la largeur..."
},
"instance.settings.title": {
"message": "Paramètres"
},
"instance.worlds.a_minecraft_server": {
"message": "Un Serveur Minecraft"
},
@@ -23,6 +23,9 @@
"app.settings.tabs.java-installations": {
"message": "התקנות ג'אווה"
},
"app.settings.tabs.language": {
"message": "שפה"
},
"app.settings.tabs.privacy": {
"message": "פרטיות"
},
@@ -80,6 +83,9 @@
"friends.add-friend.title": {
"message": "מוסיף חבר"
},
"friends.add-friend.username.description": {
"message": "זה יכול להיות שונה מהמשתמש מיינקראפט שלהם!"
},
"friends.add-friend.username.placeholder": {
"message": "הכנס שם משתמש של Modrinth..."
},
@@ -461,9 +467,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "הכנס רוחב..."
},
"instance.settings.title": {
"message": "הגדרות"
},
"instance.worlds.a_minecraft_server": {
"message": "שרת מיינקראפט"
},
+22 -19
View File
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez indítsa újra az alkalmazást, vagy zárja be a Modrinth App alkalmazást, és a frissítés automatikusan megtörténik."
},
"app.update-toast.body.linux": {
"message": "A Modrinth alkalmazás v{version} elérhető. Használd a csomagkezelődet a legújabb funkciók és hibajavítások telepítéséhez!"
},
"app.update-toast.body.metered": {
"message": "A Modrinth App v{version} már elérhető! Mivel díjköteles hálózaton vagy, így nem töltöttük le automatikusan."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Letöltés ({size})"
},
"app.update-toast.download-page": {
"message": "Letöltés"
},
"app.update-toast.downloading": {
"message": "Letöltés..."
},
@@ -237,10 +243,10 @@
"message": "Új csoport létrehozása"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "A könyvtárgyűjtemények segítenek külön kategóriákba rendszerezni a profiljaidat."
"message": "A könyvtárgyűjtemények segítenek külön kategóriákba rendszerezni a profiljait."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Add meg a gyűjtemény nevét"
"message": "Adja meg a gyűjtemény nevét"
},
"instance.settings.tabs.general.name": {
"message": "Név"
@@ -273,7 +279,7 @@
"message": "Írjon be indítás előtti parancsokat..."
},
"instance.settings.tabs.hooks.title": {
"message": "Játék indítási parancsok"
"message": "Játék indítási horgok"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Indítóparancs"
@@ -288,7 +294,7 @@
"message": "Telepítés"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "{platform} {version} már telepítve van ehhez: Minecraft {game_version}"
"message": "{platform} {version} a Minecraft {game_version} verziójához már telepítve van"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "A vanilla {game_version} már telepítve van"
@@ -333,40 +339,40 @@
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-connection": {
"message": "Nem lehetséges a csatolt modcsomag részleteit lekérdezni. Kérlek nézd meg az internetkapcsolatod."
"message": "Nem lehetséges a csatolt modpack részleteit lekérdezni. Kérlek nézd meg az internetkapcsolatod."
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "{loader} nem elérhető a Minecraft {version} verziójához. Próbálj meg egy másik modbetöltőt."
},
"instance.settings.tabs.installation.no-modpack-found": {
"message": "A profilod linkelve van egy Modrinth modcsomaghoz, de a modcsomag nem található online."
"message": "A profilod linkelve van egy Modrinth modcsomaghoz, de a modcsomag nem található Modrinth-on."
},
"instance.settings.tabs.installation.platform": {
"message": "Platform"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "Modcsomag újratelepítése"
"message": "Modpack újratelepítése"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Modcsomag újratelepítése"
"message": "Modpack újratelepítése"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Az újratelepítés visszaállítja az összes telepített vagy módosított tartalmat a modcsomag által biztosított állapotra, eltávolítva az eredeti telepítéshez hozzáadott modokat vagy tartalmakat. Ez megoldhatja a váratlan hibákat, de fontos tudni hogyha használsz világokat amelyekben utólagosan hozzáadott tartalom van, azok a világok korruptálódhatnak."
"message": "Az újratelepítés visszaállítja az összes telepített vagy módosított tartalmat a modcsomag által biztosított állapotra, eltávolítva az eredeti telepítéshez hozzáadott modokat vagy tartalmakat. Ez megoldhatja a váratlan hibákat, de fontos tudni hogyha használsz világokat amelyekben utólagosan hozzáadott tartalom van, azok a világok elromolhatnak."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Biztosan szeretnéd újratelepíteni ezt a profilt?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Visszaállítja a profilod tartalmát az eredeti állapotába, eltávolítva az eredeti modcsomaghoz hozzáadott összes modot és tartalmat."
"message": "Visszaállítja a profilod tartalmát az eredeti állapotába, eltávolítva az eredeti modpackhez hozzáadott összes modot és tartalmat."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Modcsomag újratelepítése"
"message": "Modpack újratelepítése"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Javítás"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Javítás"
"message": "Javítás folyamatban"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "A javítás újratelepíti a Minecraft alapját és ellenőrzi, hogy nincs-e sérülés. Ez megoldhatja a problémákat, ha a játék nem az indítóval kapcsolatos hibák miatt nem indul el, de nem oldja meg a telepített modokkal kapcsolatos problémákat vagy összeomlásokat."
@@ -396,13 +402,13 @@
"message": "javítás"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Nem lehet {action} telepítés közben"
"message": "Nem lehet {action}-t végrehajtani telepítés közben"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Offline állapotban nem lehet {action} végrehajtani"
"message": "Offline állapotban nem lehet {action}-t végrehajtani"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Nem lehet {action} javítás közben"
"message": "Nem lehet {action}-t végrehajtani javítás közben"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(ismeretlen verzió)"
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Szélesség megadása..."
},
"instance.settings.title": {
"message": "Beállítások"
},
"instance.worlds.a_minecraft_server": {
"message": "Egy Minecraft szerver"
},
@@ -513,7 +516,7 @@
"message": "Szerver"
},
"instance.worlds.type.singleplayer": {
"message": "Egyjátékos"
"message": "Egyjátékosmód"
},
"instance.worlds.view_instance": {
"message": "Profil megtekintése"
+23 -20
View File
@@ -38,15 +38,21 @@
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} telah selesai mengunduh. Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} tersedia. Gunakan pengelola paket Anda untuk memperbarui dan mendapatkan fitur-fitur dan perbaikan terbaru!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} sudah tersedia! Karena Anda saat ini sedang berada dalam jaringan terukur, kami tidak mengunduhnya secara otomatis."
},
"app.update-toast.changelog": {
"message": "Log perubahan"
"message": "Catatan perubahan"
},
"app.update-toast.download": {
"message": "Unduh ({size})"
},
"app.update-toast.download-page": {
"message": "Unduh"
},
"app.update-toast.downloading": {
"message": "Mengunduh..."
},
@@ -60,7 +66,7 @@
"message": "Selesai mengunduh"
},
"app.update.complete-toast.text": {
"message": "Klik di sini untuk melihat log perubahan."
"message": "Klik di sini untuk melihat catatan perubahan."
},
"app.update.complete-toast.title": {
"message": "Versi {version} berhasil dipasang!"
@@ -216,7 +222,7 @@
"message": "Gandakan wujud"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Membuat salinan dari wujud ini, termasuk dunia, konfig, modifikasi, dsb."
"message": "Membuat salinan dari wujud ini, termasuk dunia, konfigurasi, mod, dsb."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Sunting ikon"
@@ -303,7 +309,7 @@
"message": "Memasang"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Memperoleh versi paket modifikasi"
"message": "Memperoleh versi paket mod"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Memasang versi baru"
@@ -315,7 +321,7 @@
"message": "Informasi awakutu:"
},
"instance.settings.tabs.installation.fetching-modpack-details": {
"message": "Memperoleh perincian paket modifikasi"
"message": "Memperoleh perincian paket mod"
},
"instance.settings.tabs.installation.game-version": {
"message": "Versi permainan"
@@ -333,34 +339,34 @@
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-connection": {
"message": "Tidak dapat memperoleh perincian paket modifikasi terkait. Mohon periksa sambungan internet Anda."
"message": "Tidak dapat memperoleh perincian paket mod terkait. Mohon periksa sambungan internet Anda."
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "{loader} tidak tersedia untuk Minecraft {version}. Mohon coba peluncur modifikasi yang lain."
"message": "{loader} tidak tersedia untuk Minecraft {version}. Mohon coba peluncur mod yang lain."
},
"instance.settings.tabs.installation.no-modpack-found": {
"message": "Wujud ini berkaitan dengan paket modifikasi, tetapi paket modifikasi tersebut tidak dapat ditemukan pada Modrinth."
"message": "Wujud ini berkaitan dengan paket mod, tetapi paket mod tersebut tidak dapat ditemukan pada Modrinth."
},
"instance.settings.tabs.installation.platform": {
"message": "Platform"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "Pasang ulang paket modifikasi"
"message": "Pasang ulang paket mod"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Memasang ulang paket modifikasi"
"message": "Memasang ulang paket mod"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Memasang ulang akan mengatur ulang semua konten yang terpasang atau termodifikasi pada apa yang telah disediakan oleh paket modifikasi, menghapus modifikasi atau konten apa pun yang telah Anda tambahkan di atas pemasangan awal. Hal ini dapat memperbaiki perilaku tidak terduga bila ada perubahan pada wujud, tetapi bila dunia Anda sekarang bergantung pada konten terpasang tambahan, hal ini dapat merusak dunia yang ada."
"message": "Memasang ulang akan mengatur ulang semua konten yang terpasang atau termodifikasi pada apa yang telah disediakan oleh paket mod, menghapus mod atau konten apa pun yang telah Anda tambahkan di atas pemasangan awal. Hal ini dapat memperbaiki perilaku tidak terduga bila ada perubahan pada wujud, tetapi bila dunia Anda sekarang bergantung pada konten terpasang tambahan, hal ini dapat merusak dunia yang ada."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Apakah Anda yakin ingin memasang ulang wujud ini?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Mengatur ulang konten wujud menjadi keadaan awal, menghapus modifikasi atau konten apa pun yang telah Anda tambahkan di atas pemasangan awal."
"message": "Mengatur ulang konten wujud menjadi keadaan awal, menghapus mod atau konten apa pun yang telah Anda tambahkan di atas pemasangan awal."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Pasang ulang paket modifikasi"
"message": "Pasang ulang paket mod"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Perbaiki"
@@ -369,7 +375,7 @@
"message": "Memperbaiki"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Memperbaiki akan memasangkan ulang pustaka dependen dan memeriksa kerusakan. Ini dapat memecahkan masalah bila permainan Anda tidak memulai sebab kesalahan pada peluncur, tetapi tidak akan memecahkan masalah atau kemogokan yang disebabkan oleh modifikasi terpasang."
"message": "Memperbaiki akan memasangkan ulang pustaka dependen dan memeriksa kerusakan. Ini dapat memecahkan masalah bila permainan Anda tidak memulai sebab kesalahan pada peluncur, tetapi tidak akan memecahkan masalah atau kemogokan yang disebabkan oleh mod terpasang."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Perbaiki wujud?"
@@ -411,16 +417,16 @@
"message": "Lepas kaitan wujud"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Bila Anda melanjutkan, Anda tidak akan dapat mengaitkannya lagi tanpa membuat wujud baru. Anda tidak akan mendapatkan pembaruan paket modifikasi dan menjadi wujud normal."
"message": "Bila Anda melanjutkan, Anda tidak akan dapat mengaitkannya lagi tanpa membuat wujud baru. Anda tidak akan mendapatkan pembaruan paket mod dan menjadi wujud normal."
},
"instance.settings.tabs.installation.unlink.confirm.title": {
"message": "Apakah Anda yakin ingin melepas kaitan wujud ini?"
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Wujud ini berkaitan dengan paket modifikasi, yang berarti modifikasi tidak dapat diperbarui dan Anda tidak dapat mengubah versi peluncur modifikasi atau Minecraft. Melepas kaitan akan memutuskan wujud ini dari paket modifikasi secara permanen."
"message": "Wujud ini berkaitan dengan paket mod, yang berarti mod tidak dapat diperbarui dan Anda tidak dapat mengubah versi peluncur mod atau Minecraft. Melepas kaitan akan memutuskan wujud ini dari paket mod secara permanen."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Lepas kaitan dari paket modifikasi"
"message": "Lepas kaitan dari paket mod"
},
"instance.settings.tabs.java": {
"message": "Java dan memori"
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Masukkan lebar..."
},
"instance.settings.title": {
"message": "Pengaturan"
},
"instance.worlds.a_minecraft_server": {
"message": "Server Minecraft"
},
+12 -9
View File
@@ -15,10 +15,10 @@
"message": "Aspetto"
},
"app.settings.tabs.default-instance-options": {
"message": "Opzioni istanza predefinite"
"message": "Opzioni d'istanza predefinite"
},
"app.settings.tabs.feature-flags": {
"message": "Flag delle funzionalità"
"message": "Flag di funzionalità"
},
"app.settings.tabs.java-installations": {
"message": "Installazioni Java"
@@ -30,13 +30,16 @@
"message": "Privacy"
},
"app.settings.tabs.resource-management": {
"message": "Gestione risorse"
"message": "Gestione delle risorse"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} è pronta per essere installata! Ricarica per aggiornare ora, o avverrà in automatico alla chiusura di Modrinth App."
"message": "Modrinth App v{version} è pronta per essere installata! Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} è stata scaricata. Ricarica per aggiornare ora, o avverrà in automatico alla chiusura di Modrinth App."
"message": "Modrinth App v{version} è stata scaricata. Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} è disponibile. Usa il tuo gestore di pacchetti per aggiornare e ricevere le novità più recenti!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} è ora disponibile! Poiché sei su una rete a consumo, non l''abbiamo scaricata automaticamente."
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Scarica ({size})"
},
"app.update-toast.download-page": {
"message": "Scarica"
},
"app.update-toast.downloading": {
"message": "Scaricando..."
},
@@ -60,7 +66,7 @@
"message": "Download completato"
},
"app.update.complete-toast.text": {
"message": "Clicca qui per leggere le ultime novità in inglese."
"message": "Consulta qui le ultime novità in inglese."
},
"app.update.complete-toast.title": {
"message": "La versione {version} è stata installata con successo!"
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Inserisci larghezza..."
},
"instance.settings.title": {
"message": "Impostazioni"
},
"instance.worlds.a_minecraft_server": {
"message": "Un server Minecraft"
},
+8 -11
View File
@@ -12,7 +12,7 @@
"message": "v{version}をダウンロード中"
},
"app.settings.tabs.appearance": {
"message": "外観"
"message": "表示設定"
},
"app.settings.tabs.default-instance-options": {
"message": "インスタンスの基本設定"
@@ -21,25 +21,25 @@
"message": "機能設定"
},
"app.settings.tabs.java-installations": {
"message": "Javaのインストール"
"message": "Javaのインストール設定"
},
"app.settings.tabs.language": {
"message": "言語"
},
"app.settings.tabs.privacy": {
"message": "プライバシー"
"message": "プライバシー設定"
},
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.update-toast.body": {
"message": "Modrinth App v{version}は今すぐインストールできます!再起動して今すぐ更新するか、アプリを閉じた際に自動で更新されます。"
"message": "Modrinth App v{version}インストールの準備ができました。再起動して今すぐ更新するか、アプリを閉じた際に自動で更新されます。"
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version}のダウンロードが完了しました。再起動して今すぐ更新するか、アプリを閉じた際に自動で更新されます。"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version}は今すぐダウンロードできます従量課金制ネットワークを使用しているため自動ダウンロードはされていません。"
"message": "Modrinth App v{version}は今すぐダウンロードできます従量課金制ネットワークを使用しているため自動ダウンロードはされていません。"
},
"app.update-toast.changelog": {
"message": "更新履歴"
@@ -54,7 +54,7 @@
"message": "再起動"
},
"app.update-toast.title": {
"message": "アップデートが可能"
"message": "アップデートが利用可能です"
},
"app.update-toast.title.download-complete": {
"message": "ダウンロード完了"
@@ -63,7 +63,7 @@
"message": "クリックすると更新履歴を表示できます。"
},
"app.update.complete-toast.title": {
"message": "バージョン {version} インストールが正常に完了しました!"
"message": "バージョン {version} が正常にインストールされました!"
},
"app.update.download-update": {
"message": "アップデートをダウンロード"
@@ -93,7 +93,7 @@
"message": "Modrinthユーザーネームを入力..."
},
"friends.add-friend.username.title": {
"message": "あなたのフレンドのModrinthユーザーネームは何ですか?"
"message": "あなたのフレンドのModrinthユーザーは何ですか?"
},
"friends.add-friends-to-share": {
"message": "<link>友達を追加</link>して、彼らが何をしているか見てみよう!"
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "幅を入力…"
},
"instance.settings.title": {
"message": "設定"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraftサーバー"
},
+17 -17
View File
@@ -9,13 +9,13 @@
"message": "개발자 모드가 활성화되었습니다."
},
"app.settings.downloading": {
"message": "버전 다운로드 중: v{version}"
"message": "v{version} 다운로드 중"
},
"app.settings.tabs.appearance": {
"message": "모양"
},
"app.settings.tabs.default-instance-options": {
"message": "인스턴스 기본 옵션"
"message": "기본 인스턴스 옵션"
},
"app.settings.tabs.feature-flags": {
"message": "기능 플래그"
@@ -27,19 +27,22 @@
"message": "언어"
},
"app.settings.tabs.privacy": {
"message": "개인정보 보호"
"message": "개인정보"
},
"app.settings.tabs.resource-management": {
"message": "리소스 관리"
},
"app.update-toast.body": {
"message": "Modrinth App v{version}을 설치할 준비가 완료되었습니다! 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트니다."
"message": "Modrinth App v{version}을 설치할 준비가 완료되었습니다! 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트니다."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} 다운로드가 완료되었습니다. 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트니다."
"message": "Modrinth App v{version} 다운로드가 완료되었습니다. 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트니다."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} 버전을 사용할 수 있습니다. 최신 기능과 오류 수정을 적용하려면 패키지 관리자를 통해 업데이트하세요!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} 지금 이용 가능합니다! 종량제 네트워크에 있기 때문에, 자동으로 다운로드하지 않았습니다."
"message": "Modrinth App v{version} 지금 사용할 수 있습니다! 데이터 통신 연결을 사용 중이므로, 자동으로 다운로드하지 않았습니다."
},
"app.update-toast.changelog": {
"message": "변경 내역"
@@ -78,7 +81,7 @@
"message": "친구 추가"
},
"friends.action.view-friend-requests": {
"message": "{count} 친구 요청 {count, plural, one {건의} other {건의}}"
"message": "{count} 친구 요청 {count, plural, one {요청} other {요청}}"
},
"friends.add-friend.submit": {
"message": "친구 요청 보내기"
@@ -129,13 +132,13 @@
"message": "\"{query}\"와 일치하는 친구가 없습니다"
},
"friends.search-friends-placeholder": {
"message": "친구 찾는 중..."
"message": "친구 검색 중..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "친구를 추가하고 무엇을 하는지 보려면 <link>Modrinth 계정 로그인</link>하세요!"
"message": "<link>Modrinth 계정으로 로그인</link>하여 친구를 추가하고 무엇을 플레이 중인지 확인하세요!"
},
"instance.add-server.add-and-play": {
"message": "추가 후 플레이"
@@ -237,7 +240,7 @@
"message": "새 그룹 만들기"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "라이브러리 그룹을 사용하면 라이브러리 내에서 인스턴스를 서로 다른 섹션으로 구성할 수 있습니다."
"message": "라이브러리 그룹을 사용하면 인스턴스를 라이브러리 내 여러 섹션으로 나눠서 정리할 수 있습니다."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "그룹 이름 입력"
@@ -252,7 +255,7 @@
"message": "사용자 지정 실행 후크"
},
"instance.settings.tabs.hooks.description": {
"message": "후크 기능을 사용하면 고급 사용자가 게임 실행하기 전후에 특정 시스템 명령을 실행할 수 있습니다."
"message": "후크 기능을 사용하면 고급 사용자가 게임 실행 전후에 특정 시스템 명령을 실행할 수 있습니다."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "종료 후"
@@ -339,7 +342,7 @@
"message": "{loader}(은)는 Minecraft {version}에서 사용할 수 없습니다. 다른 모드 로더를 사용하세요."
},
"instance.settings.tabs.installation.no-modpack-found": {
"message": "이 인스턴스 모드팩 연결되어 있지만, 모드팩을 Modrinth에서 찾을 수 없습니다."
"message": "이 인스턴스 모드팩 연결되어 있지만, Modrinth에서 모드팩을 찾을 수 없습니다."
},
"instance.settings.tabs.installation.platform": {
"message": "플랫폼"
@@ -470,9 +473,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "너비 입력..."
},
"instance.settings.title": {
"message": "설정"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft 서버"
},
@@ -501,10 +501,10 @@
"message": "서버가 응답하지 않음"
},
"instance.worlds.no_server_quick_play": {
"message": "Minecraft 알파 1.0.5 이후 버전에서만 서버에 바로 진입할 수 있습니다."
"message": "Minecraft 알파 1.0.5 이후 버전에서만 서버에 바로 진입할 수 있습니다"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "Minecraft 1.20 이후 버전에서만 싱글플레이 세계로 바로 진입할 수 있습니다."
"message": "Minecraft 1.20 이후 버전에서만 싱글플레이 세계로 바로 진입할 수 있습니다"
},
"instance.worlds.play_instance": {
"message": "인스턴스 플레이"
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} sudah selesai dipasang! Muat semula untuk kemas kini sekarang, atau kemas kini secara automatik apabila anda menutup Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} tersedia. Gunakan pengurus pakej anda untuk mengemas kini dan mendapatkan ciri-ciri dan pembetulan terkini!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} kini tersedia! Memandangkan anda sedang menggunakan rangkaian bermeter, kami tidak memuat turunnya secara automatik."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Muat turun ({size})"
},
"app.update-toast.download-page": {
"message": "Muat turun"
},
"app.update-toast.downloading": {
"message": "Sedang memuat turun..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Masukkan lebar..."
},
"instance.settings.title": {
"message": "Tetapan"
},
"instance.worlds.a_minecraft_server": {
"message": "Pelayan Minecraft"
},
@@ -1,6 +1,6 @@
{
"app.auth-servers.unreachable.body": {
"message": "Minecraft verificatie servers zijn misschien offline. Check je internetverbinding en probeer opnieuw later."
"message": "Minecraft verificatie servers zijn mogelijk offline. Check je internetverbinding en probeer opnieuw later."
},
"app.auth-servers.unreachable.header": {
"message": "Authenticatieservers kunnen niet worden bereikt"
@@ -42,7 +42,7 @@
"message": "Modrinth App v{version} is nu beschikbaar! Omdat je nu op een netwerk met datalimiet zit, is de download niet automatisch gestart."
},
"app.update-toast.changelog": {
"message": "Changelog"
"message": "Wijzigingenlogboek"
},
"app.update-toast.download": {
"message": "Download ({size})"
@@ -60,7 +60,7 @@
"message": "Downloaden voltooid"
},
"app.update.complete-toast.text": {
"message": "Klik hier om de changelog te bekijken."
"message": "Klik hier om het wijzigingenlogboek te bekijken."
},
"app.update.complete-toast.title": {
"message": "Versie {version} is succesvol geïnstalleerd!"
@@ -201,7 +201,7 @@
"message": "Verwijder instantie"
},
"instance.settings.tabs.general.delete.description": {
"message": "Verwijdert permanent een instantie van je apparaat, inclusief je werelden, instellingen en alle geïnstalleerde inhoud. Wees voorzichtig, want zodra je een instantie verwijdert, is het niet meer mogelijk om deze te herstellen."
"message": "Verwijdert permanent een instantie van je apparaat, inclusief je werelden, instellingen en alle geïnstalleerde inhoud. Wees voorzichtig, zodra je een instantie verwijdert, is het niet meer mogelijk om deze te herstellen."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Verwijderen..."
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Voer breedte in..."
},
"instance.settings.title": {
"message": "Instellingen"
},
"instance.worlds.a_minecraft_server": {
"message": "Een Minecraft server"
},
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Angi bredde..."
},
"instance.settings.title": {
"message": "Instillinger"
},
"instance.worlds.a_minecraft_server": {
"message": "En Minecraft server"
},
@@ -33,11 +33,14 @@
"message": "Zarządzanie zasobami"
},
"app.update-toast.body": {
"message": "Wersja Modrinth App v{version} jest gotowa do pobrania! Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
"message": "Wersja Modrinth App v{version} jest gotowa do zainstalowania! Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Wersja Modrinth App v{version} została pobrana. Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} jest dostępna. Użyj menedżera pakietów, aby zaktualizować i uzyskać najnowsze funkcje i poprawki!"
},
"app.update-toast.body.metered": {
"message": "Wersja v{version} Modrinth App jest dostępna! Skoro korzystasz z sieci taryfowej, nie pobraliśmy jej automatycznie."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Pobierz ({size})"
},
"app.update-toast.download-page": {
"message": "Pobierz"
},
"app.update-toast.downloading": {
"message": "Pobieranie..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Wprowadź szerokość..."
},
"instance.settings.title": {
"message": "Ustawienia"
},
"instance.worlds.a_minecraft_server": {
"message": "Serwer Minecraft"
},
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "O Modrinth App v{version} baixado. Recarregue para atualizar agora ou a atualização será aplicada automaticamente ao fechar o Modrinth App."
},
"app.update-toast.body.linux": {
"message": "O Modrinth App v{version} está disponível. Use seu gerenciador de pacotes para atualizar e obter os recursos e correções mais recentes!"
},
"app.update-toast.body.metered": {
"message": "O Modrinth App v{version} já está disponível! Como você está em uma rede limitada, não o baixamos automaticamente."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Baixar ({size})"
},
"app.update-toast.download-page": {
"message": "Baixar"
},
"app.update-toast.downloading": {
"message": "Baixando..."
},
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Insira a largura..."
},
"instance.settings.title": {
"message": "Configurações"
},
"instance.worlds.a_minecraft_server": {
"message": "Servidor de Minecraft"
},
@@ -384,7 +384,7 @@
"message": "Mostrar todas as versões"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "alterar versão"
"message": "Alterar versão"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "instalar"
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Inserir largura..."
},
"instance.settings.title": {
"message": "Definições"
},
"instance.worlds.a_minecraft_server": {
"message": "Um Servidor de Minecraft"
},
@@ -464,9 +464,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Introdu lățimea..."
},
"instance.settings.title": {
"message": "Setări"
},
"instance.worlds.a_minecraft_server": {
"message": "Un server de Minecraft"
},
@@ -498,10 +495,10 @@
"message": "Poți să intri direct pe servere doar pe Minecraft Alpha 1.0.5+"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "Poți să intri direct în lumile singleplayer numai pe Minecraft 1.20+"
"message": "Poți să intri direct în lumile de jucător simplu numai pe Minecraft 1.20+"
},
"instance.worlds.play_instance": {
"message": "Joacă instanța"
"message": "Joacă pe instanța"
},
"instance.worlds.type.server": {
"message": "Server"
+14 -11
View File
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "Скачивание версии Modrinth App {version} завершено. Перезапустите приложение, чтобы обновить его, или оно обновится автоматически после закрытия."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} доступен. Обновите приложение, с помощью менеджера пакетов, для получения последних исправлений и улучшений!"
},
"app.update-toast.body.metered": {
"message": "Версия Modrinth App {version} доступна для скачивания! Используется сеть с лимитным тарифным планом, поэтому скачивание не началось автоматически."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Скачать ({size})"
},
"app.update-toast.download-page": {
"message": "Скачать"
},
"app.update-toast.downloading": {
"message": "Скачивание..."
},
@@ -90,7 +96,7 @@
"message": "Оно может быть не таким, как в Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Введите имя на Modrinth..."
"message": "Введите имя с Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Какое имя у друга на Modrinth?"
@@ -249,19 +255,19 @@
"message": "Команды запуска"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Изменение команд запуска"
"message": "Переопределение команд запуска"
},
"instance.settings.tabs.hooks.description": {
"message": "Позволяет опытным пользователям задать системные команды, выполняемые до и после запуска игры."
"message": "Позволяет опытным пользователям задать системные команды, выполняемые до и после запуска игры."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "После выхода"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Выполняется после выхода из игры."
"message": "Выполняется после выхода из игры."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Команда после закрытия..."
"message": "Введите команду после выхода..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Перед запуском"
@@ -420,7 +426,7 @@
"message": "Установленное содержимое связано со сборкой на Modrinth, что блокирует возможность менять версии игры, модов или загрузчик. Нажатие на кнопку ниже необратимо разрушит эту связь."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Отвязка из мод-пака"
"message": "Отвязка сборки"
},
"instance.settings.tabs.java": {
"message": "Java и память"
@@ -444,7 +450,7 @@
"message": "Окно"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Изменение настроек окна"
"message": "Переопределение настроек окна"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Полноэкранный режим"
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Введите ширину..."
},
"instance.settings.title": {
"message": "Настройки"
},
"instance.worlds.a_minecraft_server": {
"message": "Сервер Minecraft"
},
@@ -498,7 +501,7 @@
"message": "Сервер несовместим"
},
"instance.worlds.no_contact": {
"message": "Не удалось связаться с сервером"
"message": "Не удалось связаться с сервером"
},
"instance.worlds.no_server_quick_play": {
"message": "Подключаться к серверам напрямую можно только в Minecraft Alpha 1.0.5 и выше"
+18 -15
View File
@@ -21,7 +21,7 @@
"message": "Funktionsflaggor"
},
"app.settings.tabs.java-installations": {
"message": "Java installationer"
"message": "Java-installationer"
},
"app.settings.tabs.language": {
"message": "Språk"
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} har laddats ner. Ladda om för att uppdatera nu, eller automatiskt när du stänger Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} är tillgänglig. Använd din pakethanterare för att uppdatera och få tillgång till de senaste funktionerna och buggfixarna!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} är nu tillgänglig! Eftersom du använder ett nätverk med datatrafikbegränsningar har vi inte laddat ner det automatiskt."
},
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Ladda ner ({size})"
},
"app.update-toast.download-page": {
"message": "Ladda ner"
},
"app.update-toast.downloading": {
"message": "Laddar ner..."
},
@@ -303,7 +309,7 @@
"message": "Installerar"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Hämtar modpack versioner"
"message": "Hämtar modpack-versioner"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Installerar ny version"
@@ -315,10 +321,10 @@
"message": "Felsökningsinformation:"
},
"instance.settings.tabs.installation.fetching-modpack-details": {
"message": "Hämtar modpack detaljer"
"message": "Hämtar modpack-detaljer"
},
"instance.settings.tabs.installation.game-version": {
"message": "Spel version"
"message": "Spelversion"
},
"instance.settings.tabs.installation.install": {
"message": "Installera"
@@ -333,10 +339,10 @@
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-connection": {
"message": "Kan inte hämta länkade modpack detaljer. Vänligen kolla din internetanslutning."
"message": "Kan inte hämta länkade modpack-detaljer. Vänligen kontrollera din internetanslutning."
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "{loader} är inte tillgänglig för Minecraft {version}. Prova en annan mod laddare."
"message": "{loader} är inte tillgänglig för Minecraft {version}. Prova en annan modloader."
},
"instance.settings.tabs.installation.no-modpack-found": {
"message": "Denna instans är länkad till ett modpack, men modpacket kunde inte hittas på Modrinth."
@@ -351,7 +357,7 @@
"message": "Ominstallerar modpack"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Ominstallation kommer att återställa allt installerat eller modifierat innehåll till det som tillhandahålls av modpacket, och ta bort eventuella mods eller innehåll som du har lagt till ovanpå den ursprungliga installationen. Detta kan åtgärda oväntat beteende om ändringar har gjorts i instansen, men om dina världar nu är beroende av ytterligare installerat innehåll kan det skada befintliga världar."
"message": "Ominstallation kommer att återställa allt installerat eller modifierat innehåll till det som tillhandahålls av modpacket och ta bort eventuella mods eller innehåll som du har lagt till ovanpå den ursprungliga installationen. Detta kan åtgärda oväntat beteende om ändringar har gjorts i instansen, men om dina världar nu är beroende av ytterligare installerat innehåll kan det skada befintliga världar."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Är du säker att du vill ominstallera denna instans?"
@@ -417,7 +423,7 @@
"message": "Är du säker att du vill ta bort koppling till denna instans?"
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Denna instans är kopplad till ett modpack, vilket menas att mods kan inte bli uppdaterade och du kan inte ändra mod-loader eller Minecraft version. Att ta bort kopplingen kommer permanent att koppla bort denna instans från modpacket."
"message": "Denna instans är kopplad till ett modpack, vilket innebär att mods inte kan uppdateras och mod-loader eller Minecraft version inte kan ändras. Att ta bort kopplingen kommer permanent att koppla bort denna instans från modpacket."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Ta bort koppling från modpack"
@@ -429,13 +435,13 @@
"message": "Miljövariabler"
},
"instance.settings.tabs.java.hooks": {
"message": "Krokar"
"message": "Hooks"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Java argument"
"message": "Java-argument"
},
"instance.settings.tabs.java.java-installation": {
"message": "Java installation"
"message": "Java-installation"
},
"instance.settings.tabs.java.java-memory": {
"message": "Tilldelat minne"
@@ -444,7 +450,7 @@
"message": "Fönster"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Egendefinierade fönsterinställningar"
"message": "Anpassade fönsterinställningar"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Helskärm"
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Ange bredd..."
},
"instance.settings.title": {
"message": "Inställningar"
},
"instance.worlds.a_minecraft_server": {
"message": "En Minecraft Server"
},
@@ -1,6 +1,6 @@
{
"app.auth-servers.unreachable.body": {
"message": "Minecraft doğrulama sunucuları çökmüş olabilir. İnternet bağlantını kontrol et ve daha sonra tekrar dene."
"message": "Minecraft doğrulama sunucuları kapalı olabilir. İnternet bağlantını kontrol et ve daha sonra tekrar dene."
},
"app.auth-servers.unreachable.header": {
"message": "Doğrulama sunucularına erişilemedi"
@@ -84,7 +84,7 @@
"message": "Arkadaşlık isteği gönder"
},
"friends.add-friend.title": {
"message": "Bir arkadaş ekleme"
"message": "Arkadaş ekleme"
},
"friends.add-friend.username.description": {
"message": "Minecraft kullanıcı adlarından farklı olabilir!"
@@ -246,13 +246,13 @@
"message": "Ad"
},
"instance.settings.tabs.hooks": {
"message": "Başlatma bağları"
"message": "Başlatma kancaları"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Özel başlantı bağları"
"message": "Özel başlatma kancası"
},
"instance.settings.tabs.hooks.description": {
"message": "Bağlar kullanıcılara oyunu başlatmadan önce veya başlattıktan sonra sistem komutları çalıştırmalarını sağlar."
"message": "Kancalar gelişmiş kullanıcıların oyunu başlattıktan önce ve sonra belirli sistem komutları çalıştırmasını sağlar."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Çıkış-sonrası"
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Genişlik gir..."
},
"instance.settings.title": {
"message": "Ayarlar"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft Sunucusu"
},
+15 -12
View File
@@ -1,6 +1,6 @@
{
"app.auth-servers.unreachable.body": {
"message": "Сервери автентифікації Minecraft можуть зараз не працювати. Перевірте з’єднання з Інтернетом та спробуйте пізніше."
"message": "Сервери автентифікації Minecraft можуть зараз не працювати. Перевірте з’єднання з інтернетом та спробуйте пізніше."
},
"app.auth-servers.unreachable.header": {
"message": "Не вдається зв’язатися зі серверами автентифікації"
@@ -9,7 +9,7 @@
"message": "Увімкнено режим розробника."
},
"app.settings.downloading": {
"message": "Завантаження версії {version}"
"message": "Завантаження v{version}"
},
"app.settings.tabs.appearance": {
"message": "Вигляд"
@@ -33,10 +33,13 @@
"message": "Керування ресурсами"
},
"app.update-toast.body": {
"message": "Версія {version} Modrinth App готова до встановлення! Перезапустіть, щоб оновити зараз, або дозвольте оновитися автоматично, коли закриватимете Modrinth App."
"message": "Modrinth App v{version} готовий до встановлення! Перезапустіть, щоб оновити зараз. Або, оновлення буде здійснено автоматично, коли закриєте Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Версія {version} Modrinth App уже завантажилася. Перезапустіть, щоб оновити зараз, або дозвольте оновитися автоматично, коли закриватимете Modrinth App."
"message": "Modrinth App v{version} завантажено. Перезапустіть зараз, щоб оновити застосунок, або оновлення відбудеться автоматично після закриття Modrinth App."
},
"app.update-toast.body.linux": {
"message": "Доступна версія Modrinth App {version}. Скористайтеся вашим менеджером пакетів, щоб отримати найновіші функції та виправлення!"
},
"app.update-toast.body.metered": {
"message": "Версія {version} Modrinth App доступна для завантаження! Оскільки ви на лімітному з’єднанні, ми не завантажили її автоматично."
@@ -47,6 +50,9 @@
"app.update-toast.download": {
"message": "Завантажити ({size})"
},
"app.update-toast.download-page": {
"message": "Завантажити"
},
"app.update-toast.downloading": {
"message": "Завантаження…"
},
@@ -60,7 +66,7 @@
"message": "Завантаження завершено"
},
"app.update.complete-toast.text": {
"message": "Натисніть тут, щоб побачити журнал змін."
"message": "Натисніть тут, щоб переглянути журнал змін."
},
"app.update.complete-toast.title": {
"message": "Версію {version} успішно встановлено!"
@@ -72,7 +78,7 @@
"message": "Завантаження оновлення ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Перезавантажте, аби встановити оновлення"
"message": "Перезапустіть, аби встановити оновлення"
},
"friends.action.add-friend": {
"message": "Додати друга"
@@ -276,7 +282,7 @@
"message": "Гуки запуску гри"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Під час гри"
"message": "Враппер"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Обгортальна команда для запуску Minecraft."
@@ -285,10 +291,10 @@
"message": "Уведіть обгортальну команду…"
},
"instance.settings.tabs.installation": {
"message": "Установлення"
"message": "Інсталяція"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "{platform} {version} для Minecraft {game_version} вже встановлена"
"message": "{platform} {version} для Minecraft {game_version} вже встановлено"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Ванільна {game_version} вже встановлена"
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Уведіть ширину…"
},
"instance.settings.title": {
"message": "Налаштування"
},
"instance.worlds.a_minecraft_server": {
"message": "Сервер Minecraft"
},
@@ -470,9 +470,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "Nhập chiều dài..."
},
"instance.settings.title": {
"message": "Thiết đặt"
},
"instance.worlds.a_minecraft_server": {
"message": "Một máy chủ Minecraft"
},
+21 -18
View File
@@ -38,6 +38,9 @@
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} 已经完成下载。现在重新启动以更新,或在关闭 Modrinth App 时自动更新。"
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} 现已发布。请使用你的软件包管理器进行更新,获取最新功能与修复!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} 现已发布!由于你正在使用按流量计费的网络,该更新未自动下载。"
},
@@ -47,11 +50,14 @@
"app.update-toast.download": {
"message": "下载({size}"
},
"app.update-toast.download-page": {
"message": "下载"
},
"app.update-toast.downloading": {
"message": "下载中…"
},
"app.update-toast.reload": {
"message": "重启应用"
"message": "重新启动"
},
"app.update-toast.title": {
"message": "有可用更新"
@@ -231,13 +237,13 @@
"message": "选择图标"
},
"instance.settings.tabs.general.library-groups": {
"message": "实例分组"
"message": "分组"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "创建新的分组"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "实例分组可将你的实例整理到实例库的不同版块中。"
"message": "库分组功能可以让你将实例整理到库中的不同部分。"
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "输入分组名称"
@@ -255,13 +261,13 @@
"message": "Hooks允许高级用户在启动游戏前后运行特定的系统命令。"
},
"instance.settings.tabs.hooks.post-exit": {
"message": "退出后"
"message": "退出后执行"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "在游戏关闭后运行。"
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "输入退出后运行的命令…"
"message": "输入退出后运行的命令…"
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "启动前"
@@ -270,7 +276,7 @@
"message": "在实例启动前运行。"
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "输入启动命令..."
"message": "输入启动命令……"
},
"instance.settings.tabs.hooks.title": {
"message": "游戏启动钩子"
@@ -357,7 +363,7 @@
"message": "你确定要重新安装该实例吗?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "重置实例至原始状态,移除你在原始整合包上添加的所有模组及内容。"
"message": "将实例的内容重置为原始状态,移除你在原始整合包基础上添加的任何修改或内容。"
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "重新安装整合包"
@@ -369,7 +375,7 @@
"message": "修复中"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "修复实例将重新安装 Minecraft 依赖并检查文件完整性。该操作有可能解决因启动器错误导致的启动失败问题,但不解决由模组引发的问题或崩溃。"
"message": "修复重新安装 Minecraft 依赖并检查是否有损坏。如果你的游戏因启动器相关错误无法启动,这可能会解决问题,但不解决与已安装模组相关的问题或崩溃。"
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "确定修复该实例吗?"
@@ -408,19 +414,19 @@
"message": "(未知版本)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "解除关联实例"
"message": "解除实例关联"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "若继续操作,该实例将无法重新关联至原整合包,除非创建全新的实例。此后,不再接收整合包的任何更新,该实例变为普通实例。"
},
"instance.settings.tabs.installation.unlink.confirm.title": {
"message": "你确定要解除关联实例吗?"
"message": "你确定要取消关联实例吗?"
},
"instance.settings.tabs.installation.unlink.description": {
"message": "该实例已关联至整合包,因此无法单独更新模组,也无法更改模组加载器和 Minecraft 版本。解除关联后,该实例将无法重新关联至整合包。"
},
"instance.settings.tabs.installation.unlink.title": {
"message": "解除整合包关联"
"message": "取消整合包关联"
},
"instance.settings.tabs.java": {
"message": "Java 及内存"
@@ -459,7 +465,7 @@
"message": "游戏启动时的窗口高度。"
},
"instance.settings.tabs.window.height.enter": {
"message": "输入高度…"
"message": "输入高度…"
},
"instance.settings.tabs.window.width": {
"message": "宽度"
@@ -468,10 +474,7 @@
"message": "游戏启动时的窗口宽度。"
},
"instance.settings.tabs.window.width.enter": {
"message": "输入宽度…"
},
"instance.settings.title": {
"message": "设置"
"message": "输入宽度…"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft 服务器"
@@ -483,7 +486,7 @@
"message": "复制地址"
},
"instance.worlds.dont_show_on_home": {
"message": "在主界面不显示"
"message": "在主界面隐藏"
},
"instance.worlds.filter.available": {
"message": "可用"
@@ -519,7 +522,7 @@
"message": "查看实例"
},
"instance.worlds.world_in_use": {
"message": "世界用中"
"message": "世界正在使用中"
},
"search.filter.locked.instance": {
"message": "由该实例提供"
+13 -10
View File
@@ -38,15 +38,21 @@
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} 已完成下載。重新載入立即更新,或是在你關閉 Modrinth App 時自動更新。"
},
"app.update-toast.body.linux": {
"message": "Modrinth App v{version} 現已推出。請使用您的套件管理員進行更新,以取得最新的功能與修正!"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} 現已推出!由於你正使用計量付費的網路,我們沒有自動下載它。"
},
"app.update-toast.changelog": {
"message": "變更紀錄"
"message": "更新日誌"
},
"app.update-toast.download": {
"message": "下載 ({size})"
},
"app.update-toast.download-page": {
"message": "下載"
},
"app.update-toast.downloading": {
"message": "下載中..."
},
@@ -60,7 +66,7 @@
"message": "下載完成"
},
"app.update.complete-toast.text": {
"message": "按這裡查看變更紀錄。"
"message": "按這裡查看更新日誌。"
},
"app.update.complete-toast.title": {
"message": "版本 {version} 已成功安裝!"
@@ -78,10 +84,10 @@
"message": "新增好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 個好友請"
"message": "{count} 個好友請"
},
"friends.add-friend.submit": {
"message": "傳送好友請求"
"message": "送出好友邀請"
},
"friends.add-friend.title": {
"message": "新增好友"
@@ -105,7 +111,7 @@
"message": "移除好友"
},
"friends.friend.request-sent": {
"message": "已送出好友請"
"message": "已送出好友請"
},
"friends.friend.view-profile": {
"message": "查看個人檔案"
@@ -324,7 +330,7 @@
"message": "安裝"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "安裝進行中"
"message": "安裝中"
},
"instance.settings.tabs.installation.loader-version": {
"message": "{loader} 版本"
@@ -375,7 +381,7 @@
"message": "是否要修復實例?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "修復進行中"
"message": "修復中"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "重設為目前版本"
@@ -470,9 +476,6 @@
"instance.settings.tabs.window.width.enter": {
"message": "輸入寬度..."
},
"instance.settings.title": {
"message": "設定"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft 伺服器"
},
+5 -2
View File
@@ -9,6 +9,7 @@ import {
injectNotificationManager,
LoadingIndicator,
Pagination,
ProjectCardList,
SearchFilterControl,
SearchSidebarFilter,
useSearch,
@@ -509,10 +510,12 @@ previousFilterState.value = JSON.stringify({
<section v-else-if="offline && results.total_hits === 0" class="offline">
You are currently offline. Connect to the internet to browse Modrinth!
</section>
<section v-else class="project-list display-mode--list instance-results" role="list">
<ProjectCardList v-else :layout="'list'">
<SearchCard
v-for="result in results.hits"
:key="result?.project_id"
:project-type="projectType"
:project="result"
:instance="instance"
:categories="[
@@ -538,7 +541,7 @@ previousFilterState.value = JSON.stringify({
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
</ContextMenu>
</section>
</ProjectCardList>
<div class="flex justify-end">
<pagination
:page="currentPage"
@@ -6,6 +6,10 @@
>
<ExportModal ref="exportModal" :instance="instance" />
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
<ButtonStyled v-if="themeStore.featureFlags.server_project_qa">
<button @click="updateToPlayModal.show()">Update to play modal</button>
</ButtonStyled>
<ContentPageHeader>
<template #icon>
<Avatar :src="icon" :alt="instance.name" size="96px" :tint-by="instance.path" />
@@ -198,6 +202,7 @@ import { useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import ExportModal from '@/components/ui/ExportModal.vue'
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project, get_version_many } from '@/helpers/cache.js'
@@ -206,7 +211,9 @@ import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import { showProfileInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
import { useBreadcrumbs, useLoading } from '@/store/state'
import { useBreadcrumbs, useLoading, useTheming } from '@/store/state'
const themeStore = useTheming()
dayjs.extend(duration)
dayjs.extend(relativeTime)
@@ -229,6 +236,7 @@ const instance = ref()
const modrinthVersions = ref([])
const playing = ref(false)
const loading = ref(false)
const updateToPlayModal = ref()
async function fetchInstance() {
instance.value = await get(route.params.id).catch(handleError)
@@ -1,5 +1,6 @@
<template>
<div>
<InstallToPlayModal ref="installToPlayModal" :project="data" />
<Teleport to="#sidebar-teleport-target">
<ProjectSidebarCompatibility
:project="data"
@@ -23,6 +24,9 @@
/>
</Teleport>
<div class="flex flex-col gap-4 p-6">
<ButtonStyled v-if="themeStore.featureFlags.server_project_qa">
<button @click="installToPlayModal.show()">Install to play modal</button>
</ButtonStyled>
<InstanceIndicator v-if="instance" :instance="instance" />
<template v-if="data">
<Teleport
@@ -159,6 +163,7 @@ import { useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
@@ -186,6 +191,8 @@ const instanceProjects = ref(null)
const installed = ref(false)
const installedVersion = ref(null)
const installToPlayModal = ref()
const instanceFilters = computed(() => {
if (!instance.value) {
return {}
+1
View File
@@ -6,6 +6,7 @@ export const DEFAULT_FEATURE_FLAGS = {
worlds_tab: false,
worlds_in_home: true,
servers_in_app: false,
server_project_qa: false,
}
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
+2 -2
View File
@@ -38,10 +38,10 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
async fn main() -> theseus::Result<()> {
println!("Starting.");
let _log_guard = theseus::start_logger();
let _log_guard = theseus::start_logger("ModrinthApp");
// Initialize state
State::init().await?;
State::init("ModrinthApp".to_owned()).await?;
let worlds = get_recent_worlds(4, EnumSet::all()).await?;
for world in worlds {
+4 -1
View File
@@ -29,6 +29,9 @@ pub async fn settings_set(settings: Settings) -> Result<()> {
#[tauri::command]
pub async fn cancel_directory_change() -> Result<()> {
settings::cancel_directory_change().await?;
let state = State::get().await?;
let identifier = &state.app_identifier;
settings::cancel_directory_change(identifier).await?;
Ok(())
}
+6 -4
View File
@@ -107,10 +107,12 @@ pub async fn open_path<R: Runtime>(app: tauri::AppHandle<R>, path: PathBuf) {
#[tauri::command]
pub async fn show_launcher_logs_folder<R: Runtime>(app: tauri::AppHandle<R>) {
let path = DirectoryInfo::launcher_logs_dir().unwrap_or_default();
// failure to get folder just opens filesystem
// (ie: if in debug mode only and launcher_logs never created)
open_path(app, path).await;
if let Some(d) = DirectoryInfo::global_handle_if_ready() {
let path = d.launcher_logs_dir().unwrap_or_default();
// failure to get folder just opens filesystem
// (ie: if in debug mode only and launcher_logs never created)
open_path(app, path).await;
}
}
// Get opening command
+6 -3
View File
@@ -28,7 +28,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app.clone()).await?;
tracing::info!("Initializing app state...");
State::init().await?;
State::init(app.config().identifier.clone()).await?;
let state = State::get().await?;
app.asset_protocol_scope()
@@ -110,7 +110,10 @@ fn main() {
RUST_LOG="theseus=trace" {run command}
*/
let _log_guard = theseus::start_logger();
let tauri_context = tauri::generate_context!();
let _log_guard = theseus::start_logger(&tauri_context.config().identifier);
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!");
@@ -243,7 +246,7 @@ fn main() {
]);
tracing::info!("Initializing app...");
let app = builder.build(tauri::generate_context!());
let app = builder.build(tauri_context);
match app {
Ok(app) => {
+1 -1
View File
@@ -87,7 +87,7 @@
"capabilities": ["ads", "core", "plugins"],
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:",
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net 'self' data: blob:",
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
"style-src": "'unsafe-inline' 'self'",
+1 -1
View File
@@ -180,7 +180,7 @@ export default defineNuxtConfig({
console.log('Tags generated!')
const robotsContent =
getDomain() === PROD_MODRINTH_URL
getDomain() === PROD_MODRINTH_URL && process.env.PREVIEW !== 'true'
? 'User-agent: *\nDisallow: /_internal/'
: 'User-agent: *\nDisallow: /'
+2 -2
View File
@@ -11,8 +11,8 @@
"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",
"cf-deploy": "pnpm run build && wrangler deploy --env preview",
"cf-dev": "pnpm run build && wrangler dev --env preview",
"cf-deploy": "pnpm run build && wrangler deploy --env staging",
"cf-dev": "pnpm run build && wrangler dev --env staging",
"cf-typegen": "wrangler types"
},
"devDependencies": {
@@ -223,10 +223,7 @@
:where(input) {
box-sizing: border-box;
max-height: 40px;
&:not(.stylized-toggle) {
max-width: 100%;
}
max-width: 100%;
}
:where(.adjacent-input, &.adjacent-input) {
@@ -271,10 +268,6 @@
&:not(&.small) {
flex-direction: column;
align-items: flex-start;
.stylized-toggle {
flex-basis: 0;
}
}
}
}
@@ -650,64 +643,6 @@ tr.button-transparent {
}
}
.switch {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
&:focus {
//outline: 0; Bad for accessibility
}
}
.stylized-toggle {
@extend .button-base;
box-sizing: content-box;
min-height: 32px;
height: 32px;
width: 52px;
max-width: 52px;
border-radius: var(--size-rounded-max);
display: inline-block;
position: relative;
margin: 0;
transition: all 0.2s ease;
background: var(--color-button-bg);
&:after {
content: '';
position: absolute;
top: 7px;
left: 7px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-toggle-handle);
transition: all 0.2s cubic-bezier(0.5, 0.1, 0.75, 1.35);
outline: 2px solid transparent;
@media (prefers-reduced-motion) {
transition: none;
}
}
&:checked {
background-color: var(--color-brand);
&:after {
transform: translatex(20px);
background: var(--color-brand-inverted);
}
}
&:hover &:focus {
background: var(--color-button-bg);
}
}
.textarea-wrapper {
display: flex;
flex-direction: column;
@@ -60,8 +60,6 @@ html {
--color-button-bg-active: #c3c6cb;
--color-button-text-active: var(--color-button-text-hover);
--color-toggle-handle: var(--color-icon);
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);
@@ -177,8 +175,6 @@ html {
--color-button-bg-active: #616570;
--color-button-text-active: var(--color-button-text-hover);
--color-toggle-handle: var(--color-button-text);
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);
@@ -471,14 +467,6 @@ kbd {
@import '~/assets/styles/utils.scss';
@import '~/assets/styles/components.scss';
button:focus-visible,
a:focus-visible,
[tabindex='0']:focus-visible,
[type='button']:focus-visible {
outline: 0.25rem solid #ea80ff;
border-radius: 0.25rem;
}
// OMORPHIA FIXES
.card {
outline-offset: -2px;
+1 -1
View File
@@ -87,7 +87,7 @@ export default {
}
&:focus-visible {
outline: 0.25rem solid #ea80ff;
outline: 0.25rem solid var(--color-focus-ring);
border-radius: 0.25rem;
}
}
+186 -146
View File
@@ -2,78 +2,54 @@
<nav
ref="scrollContainer"
class="experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
:class="[mode === 'navigation' ? 'card-shadow' : undefined]"
:class="{ 'card-shadow': mode === 'navigation' }"
>
<template v-if="mode === 'navigation'">
<NuxtLink
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
v-show="link.shown ?? true"
:key="link.href"
ref="tabLinkElements"
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
:class="getSSRFallbackClasses(index)"
@mouseenter="link.onHover?.()"
@focus="link.onHover?.()"
>
<component
:is="link.icon"
v-if="link.icon"
class="size-5"
:class="{
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
'text-secondary': currentActiveIndex !== index || subpageSelected,
}"
/>
<span
class="text-nowrap"
:class="{
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
'text-contrast': currentActiveIndex !== index || subpageSelected,
}"
>{{ link.label }}</span
>
<component :is="link.icon" v-if="link.icon" class="size-5" :class="getIconClasses(index)" />
<span class="text-nowrap" :class="getLabelClasses(index)">
{{ link.label }}
</span>
</NuxtLink>
</template>
<template v-else>
<div
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
v-show="link.shown ?? true"
:key="link.href"
ref="tabLinkElements"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 hover:cursor-pointer focus:rounded-full"
:class="getSSRFallbackClasses(index)"
@click="emit('tabClick', index, link)"
>
<component
:is="link.icon"
v-if="link.icon"
class="size-5"
:class="{
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
'text-secondary': currentActiveIndex !== index || subpageSelected,
}"
/>
<span
class="text-nowrap"
:class="{
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
'text-contrast': currentActiveIndex !== index || subpageSelected,
}"
>{{ link.label }}</span
>
<component :is="link.icon" v-if="link.icon" class="size-5" :class="getIconClasses(index)" />
<span class="text-nowrap" :class="getLabelClasses(index)">
{{ link.label }}
</span>
</div>
</template>
<!-- Animated slider background -->
<div
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'
}`"
:style="{
left: sliderLeftPx,
top: sliderTopPx,
right: sliderRightPx,
bottom: sliderBottomPx,
opacity:
sliderLeft === 4 && sliderLeft === sliderRight ? 0 : currentActiveIndex === -1 ? 0 : 1,
}"
class="pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1"
:class="[
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected',
{ 'navtabs-transition': transitionsEnabled },
]"
:style="sliderStyle"
aria-hidden="true"
></div>
/>
</nav>
</template>
@@ -89,6 +65,7 @@ interface Tab {
shown?: boolean
icon?: Component
subpages?: string[]
onHover?: () => void
}
const props = withDefaults(
@@ -109,124 +86,194 @@ const emit = defineEmits<{
tabClick: [index: number, tab: Tab]
}>()
// DOM refs
const scrollContainer = ref<HTMLElement | null>(null)
const tabLinkElements = ref<HTMLElement[]>()
// Slider pos state
const sliderLeft = ref(4)
const sliderTop = ref(4)
const sliderRight = ref(4)
const sliderBottom = ref(4)
// active tab state
const currentActiveIndex = ref(-1)
const subpageSelected = ref(false)
const filteredLinks = computed(() =>
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
// SSR state
const sliderReady = ref(false) // Slider is positioned and should be visible
const transitionsEnabled = ref(false) // CSS transitions should apply (after first paint)
const filteredLinks = computed(() => props.links.filter((link) => link.shown ?? true))
const sliderStyle = computed(() => ({
left: `${sliderLeft.value}px`,
top: `${sliderTop.value}px`,
right: `${sliderRight.value}px`,
bottom: `${sliderBottom.value}px`,
opacity: sliderReady.value && currentActiveIndex.value !== -1 ? 1 : 0,
}))
const isActiveAndNotSubpage = computed(
() => (index: number) => currentActiveIndex.value === index && !subpageSelected.value,
)
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
const sliderTopPx = computed(() => `${sliderTop.value}px`)
const sliderRightPx = computed(() => `${sliderRight.value}px`)
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
const tabLinkElements = ref()
function getSSRFallbackClasses(index: number) {
if (sliderReady.value) return {}
if (currentActiveIndex.value !== index) return {}
function pickLink() {
let index = -1
subpageSelected.value = false
return {
'rounded-full': true,
'bg-button-bgSelected': !subpageSelected.value,
'bg-button-bg': subpageSelected.value,
}
}
function getIconClasses(index: number) {
return {
'text-button-textSelected': isActiveAndNotSubpage.value(index),
'text-secondary': !isActiveAndNotSubpage.value(index),
}
}
function getLabelClasses(index: number) {
return {
'text-button-textSelected': isActiveAndNotSubpage.value(index),
'text-contrast': !isActiveAndNotSubpage.value(index),
}
}
function computeActiveIndex(): { index: number; isSubpage: boolean } {
if (props.mode === 'local' && props.activeIndex !== undefined) {
index = Math.min(props.activeIndex, filteredLinks.value.length - 1)
} else {
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
if (props.query) {
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {
index = i
break
}
} else if (decodeURIComponent(route.path) === link.href) {
index = i
break
} else if (
decodeURIComponent(route.path).includes(link.href) ||
(link.subpages &&
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
) {
index = i
subpageSelected.value = true
break
}
return {
index: Math.min(props.activeIndex, filteredLinks.value.length - 1),
isSubpage: false,
}
}
currentActiveIndex.value = index
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
const decodedPath = decodeURIComponent(route.path)
if (currentActiveIndex.value !== -1) {
nextTick(() => startAnimation())
// Query-based matching
if (props.query) {
const queryValue = route.query[props.query]
if (queryValue === link.href || (!queryValue && !link.href)) {
return { index: i, isSubpage: false }
}
continue
}
// Exact path match
if (decodedPath === link.href) {
return { index: i, isSubpage: false }
}
// Subpage match
const isSubpageMatch =
decodedPath.includes(link.href) ||
link.subpages?.some((subpage) => decodedPath.includes(subpage))
if (isSubpageMatch) {
return { index: i, isSubpage: true }
}
}
return { index: -1, isSubpage: false }
}
function getTabElement(index: number): HTMLElement | null {
if (!tabLinkElements.value?.[index]) return null
// In navigation mode, elements are NuxtLinks with $el property
// In local mode, elements are plain divs
const element = tabLinkElements.value[index]
return props.mode === 'navigation' ? (element as any).$el : element
}
function positionSlider() {
const el = getTabElement(currentActiveIndex.value)
if (!el?.offsetParent) return
const parent = el.offsetParent as HTMLElement
const newPosition = {
left: el.offsetLeft,
top: el.offsetTop,
right: parent.offsetWidth - el.offsetLeft - el.offsetWidth,
bottom: parent.offsetHeight - el.offsetTop - el.offsetHeight,
}
const isInitialPosition = sliderLeft.value === 4 && sliderRight.value === 4
if (isInitialPosition) {
// Initial positioning: set position instantly, no animation
sliderLeft.value = newPosition.left
sliderRight.value = newPosition.right
sliderTop.value = newPosition.top
sliderBottom.value = newPosition.bottom
sliderReady.value = true
// enable transitions after slider is painted, so future changes animate
requestAnimationFrame(() => {
transitionsEnabled.value = true
})
} else {
animateSliderTo(newPosition)
}
}
function animateSliderTo(newPosition: {
left: number
top: number
right: number
bottom: number
}) {
const STAGGER_DELAY = 200
// Horizontal animation - lead with the direction of movement
if (newPosition.left < sliderLeft.value) {
sliderLeft.value = newPosition.left
setTimeout(() => (sliderRight.value = newPosition.right), STAGGER_DELAY)
} else {
sliderRight.value = newPosition.right
setTimeout(() => (sliderLeft.value = newPosition.left), STAGGER_DELAY)
}
// Vertical animation - lead with the direction of movement
if (newPosition.top < sliderTop.value) {
sliderTop.value = newPosition.top
setTimeout(() => (sliderBottom.value = newPosition.bottom), STAGGER_DELAY)
} else {
sliderBottom.value = newPosition.bottom
setTimeout(() => (sliderTop.value = newPosition.top), STAGGER_DELAY)
}
}
function updateActiveTab() {
const { index, isSubpage } = computeActiveIndex()
currentActiveIndex.value = index
subpageSelected.value = isSubpage
if (index !== -1) {
nextTick(positionSlider)
} else {
sliderLeft.value = 0
sliderRight.value = 0
}
}
function startAnimation() {
// In navigation mode, elements are NuxtLinks with $el property
// In local mode, elements are plain divs
const el =
props.mode === 'navigation'
? tabLinkElements.value[currentActiveIndex.value]?.$el
: tabLinkElements.value[currentActiveIndex.value]
const initialActive = computeActiveIndex()
currentActiveIndex.value = initialActive.index
subpageSelected.value = initialActive.isSubpage
if (!el || !el.offsetParent) return
const newValues = {
left: el.offsetLeft,
top: el.offsetTop,
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
}
if (sliderLeft.value === 4 && sliderRight.value === 4) {
sliderLeft.value = newValues.left
sliderRight.value = newValues.right
sliderTop.value = newValues.top
sliderBottom.value = newValues.bottom
} else {
const delay = 200
if (newValues.left < sliderLeft.value) {
sliderLeft.value = newValues.left
setTimeout(() => {
sliderRight.value = newValues.right
}, delay)
} else {
sliderRight.value = newValues.right
setTimeout(() => {
sliderLeft.value = newValues.left
}, delay)
}
if (newValues.top < sliderTop.value) {
sliderTop.value = newValues.top
setTimeout(() => {
sliderBottom.value = newValues.bottom
}, delay)
} else {
sliderBottom.value = newValues.bottom
setTimeout(() => {
sliderTop.value = newValues.top
}, delay)
}
}
}
onMounted(() => {
pickLink()
})
onMounted(updateActiveTab)
watch(
() => [route.path, route.query],
() => {
if (props.mode === 'navigation') {
pickLink()
updateActiveTab()
}
},
)
@@ -235,19 +282,12 @@ watch(
() => props.activeIndex,
() => {
if (props.mode === 'local') {
pickLink()
updateActiveTab()
}
},
)
watch(
() => props.links,
() => {
// Re-trigger animation when links change
pickLink()
},
{ deep: true },
)
watch(() => props.links, updateActiveTab, { deep: true })
</script>
<style scoped>
@@ -458,9 +458,11 @@ function getMessages() {
}
function getLoaderCategories(ver) {
return tags.value.loaders.filter((loader) => {
return ver?.loaders?.includes(loader.name)
})
return tags.value.loaders
.filter((loader) => {
return ver?.loaders?.includes(loader.name)
})
.map((loader) => loader.name)
}
</script>
@@ -1,527 +0,0 @@
<template>
<article class="project-card base-card padding-bg" :aria-label="name" role="listitem">
<nuxt-link
:title="name"
class="icon"
tabindex="-1"
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
>
<Avatar :src="iconUrl" :alt="name" size="md" no-shadow loading="lazy" />
</nuxt-link>
<nuxt-link
class="gallery"
:class="{ 'no-image': !featuredImage }"
tabindex="-1"
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
:style="color ? `background-color: ${toColor};` : ''"
>
<img v-if="featuredImage" :src="featuredImage" alt="gallery image" loading="lazy" />
</nuxt-link>
<div class="title">
<nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`">
<h2 class="name !text-2xl">
{{ name }}
</h2>
</nuxt-link>
<p v-if="author" class="author">
by
<nuxt-link class="title-link" :to="'/user/' + author">
{{ author }}
</nuxt-link>
</p>
<ProjectStatusBadge v-if="status && status !== 'approved'" :status="status" class="status" />
</div>
<p class="description">
{{ description }}
</p>
<Categories
:categories="
categories.filter((x) => !hideLoaders || !tags.loaders.find((y) => y.name === x))
"
:type="type"
class="tags"
>
<EnvironmentIndicator
v-if="clientSide && serverSide"
:type-only="moderation"
:client-side="clientSide"
:server-side="serverSide"
:type="projectTypeDisplay"
:search="search"
:categories="categories"
/>
</Categories>
<div class="stats">
<div v-if="downloads" class="stat">
<DownloadIcon aria-hidden="true" />
<p>
<strong>{{ $formatNumber(downloads) }}</strong
><span class="stat-label"> download<span v-if="downloads !== '1'">s</span></span>
</p>
</div>
<div v-if="follows" class="stat">
<HeartIcon aria-hidden="true" />
<p>
<strong>{{ $formatNumber(follows) }}</strong
><span class="stat-label"> follower<span v-if="follows !== '1'">s</span></span>
</p>
</div>
<div class="buttons">
<slot />
</div>
<div
v-if="showUpdatedDate"
v-tooltip="$dayjs(updatedAt).format('MMMM D, YYYY [at] h:mm A')"
class="stat date"
>
<UpdatedIcon aria-hidden="true" />
<span class="date-label">Updated </span>{{ formatRelativeTime(updatedAt) }}
</div>
<div
v-else-if="showCreatedDate"
v-tooltip="$dayjs(createdAt).format('MMMM D, YYYY [at] h:mm A')"
class="stat date"
>
<CalendarIcon aria-hidden="true" />
<span class="date-label">Published </span>{{ formatRelativeTime(createdAt) }}
</div>
</div>
</article>
</template>
<script>
import { CalendarIcon, DownloadIcon, HeartIcon, UpdatedIcon } from '@modrinth/assets'
import { Avatar, ProjectStatusBadge, useRelativeTime } from '@modrinth/ui'
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
import Categories from '~/components/ui/search/Categories.vue'
export default {
components: {
ProjectStatusBadge,
EnvironmentIndicator,
Avatar,
Categories,
CalendarIcon,
UpdatedIcon,
DownloadIcon,
HeartIcon,
},
props: {
id: {
type: String,
default: 'modrinth-0',
},
type: {
type: String,
default: 'mod',
},
name: {
type: String,
default: 'Project Name',
},
author: {
type: String,
default: null,
},
description: {
type: String,
default: 'A _type description',
},
iconUrl: {
type: String,
default: '#',
required: false,
},
downloads: {
type: String,
default: null,
required: false,
},
follows: {
type: String,
default: null,
required: false,
},
createdAt: {
type: String,
default: '0000-00-00',
},
updatedAt: {
type: String,
default: null,
},
categories: {
type: Array,
default() {
return []
},
},
status: {
type: String,
default: null,
},
hasModMessage: {
type: Boolean,
default: false,
},
serverSide: {
type: String,
required: false,
default: '',
},
clientSide: {
type: String,
required: false,
default: '',
},
moderation: {
type: Boolean,
required: false,
default: false,
},
search: {
type: Boolean,
required: false,
default: false,
},
featuredImage: {
type: String,
required: false,
default: null,
},
showUpdatedDate: {
type: Boolean,
required: false,
default: true,
},
showCreatedDate: {
type: Boolean,
required: false,
default: true,
},
hideLoaders: {
type: Boolean,
required: false,
default: false,
},
color: {
type: Number,
required: false,
default: null,
},
},
setup() {
const tags = useGeneratedState()
const formatRelativeTime = useRelativeTime()
return { tags, formatRelativeTime }
},
computed: {
projectTypeDisplay() {
return this.$getProjectTypeForDisplay(this.type, this.categories)
},
toColor() {
let color = this.color
color >>>= 0
const b = color & 0xff
const g = (color & 0xff00) >>> 8
const r = (color & 0xff0000) >>> 16
return 'rgba(' + [r, g, b, 1].join(',') + ')'
},
},
}
</script>
<style lang="scss" scoped>
.project-card {
display: inline-grid;
box-sizing: border-box;
overflow: hidden;
margin: 0;
}
.display-mode--list .project-card {
grid-template:
'icon title stats'
'icon description stats'
'icon tags stats';
grid-template-columns: min-content 1fr auto;
grid-template-rows: min-content 1fr min-content;
column-gap: var(--spacing-card-md);
row-gap: var(--spacing-card-sm);
width: 100%;
@media screen and (max-width: 750px) {
grid-template:
'icon title'
'icon description'
'icon tags'
'stats stats';
grid-template-columns: min-content auto;
grid-template-rows: min-content 1fr min-content min-content;
}
@media screen and (max-width: 550px) {
grid-template:
'icon title'
'icon description'
'tags tags'
'stats stats';
grid-template-columns: min-content auto;
grid-template-rows: min-content 1fr min-content min-content;
}
}
.display-mode--gallery .project-card,
.display-mode--grid .project-card {
padding: 0 0 var(--spacing-card-bg) 0;
grid-template: 'gallery gallery' 'icon title' 'description description' 'tags tags' 'stats stats';
grid-template-columns: min-content 1fr;
grid-template-rows: min-content min-content 1fr min-content min-content;
row-gap: var(--spacing-card-sm);
.gallery {
display: inline-block;
width: 100%;
height: 10rem;
background-color: var(--color-button-bg-active);
&.no-image {
filter: brightness(0.7);
}
img {
box-shadow: none;
width: 100%;
height: 10rem;
object-fit: cover;
}
}
.icon {
margin-left: var(--spacing-card-bg);
margin-top: -3rem;
z-index: 1;
img,
svg {
border-radius: var(--size-rounded-lg);
box-shadow:
-2px -2px 0 2px var(--color-raised-bg),
2px -2px 0 2px var(--color-raised-bg);
}
}
.title {
margin-left: var(--spacing-card-md);
margin-right: var(--spacing-card-bg);
flex-direction: column;
.name {
font-size: 1.25rem;
}
.status {
margin-top: var(--spacing-card-xs);
}
}
.description {
margin-inline: var(--spacing-card-bg);
}
.tags {
margin-inline: var(--spacing-card-bg);
}
.stats {
margin-inline: var(--spacing-card-bg);
flex-direction: row;
align-items: center;
.stat-label {
display: none;
}
.buttons {
flex-direction: row;
gap: var(--spacing-card-sm);
align-items: center;
> :first-child {
margin-left: auto;
}
&:first-child > :last-child {
margin-right: auto;
}
}
.buttons:not(:empty) + .date {
flex-basis: 100%;
}
}
}
.display-mode--grid .project-card {
.gallery {
display: none;
}
.icon {
margin-top: calc(var(--spacing-card-bg) - var(--spacing-card-sm));
img,
svg {
border: none;
}
}
.title {
margin-top: calc(var(--spacing-card-bg) - var(--spacing-card-sm));
}
}
.icon {
grid-area: icon;
display: flex;
align-items: center;
}
.gallery {
display: none;
height: 10rem;
grid-area: gallery;
}
.title {
grid-area: title;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
column-gap: var(--spacing-card-sm);
row-gap: 0;
word-wrap: anywhere;
h2,
p {
margin: 0;
}
svg {
width: auto;
color: var(--color-orange);
height: 1.5rem;
margin-bottom: -0.25rem;
}
}
.stats {
grid-area: stats;
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: flex-end;
gap: var(--spacing-card-md);
.stat {
display: flex;
flex-direction: row;
align-items: center;
width: fit-content;
gap: var(--spacing-card-xs);
--stat-strong-size: 1.25rem;
strong {
font-size: var(--stat-strong-size);
}
p {
margin: 0;
}
svg {
height: var(--stat-strong-size);
width: var(--stat-strong-size);
}
}
.date {
margin-top: auto;
}
@media screen and (max-width: 750px) {
flex-direction: row;
column-gap: var(--spacing-card-md);
margin-top: var(--spacing-card-xs);
}
@media screen and (max-width: 600px) {
margin-top: 0;
.stat-label {
display: none;
}
}
}
.environment {
color: var(--color-text) !important;
font-weight: bold;
}
.description {
grid-area: description;
margin-block: 0;
display: flex;
justify-content: flex-start;
word-break: break-word;
}
.tags {
grid-area: tags;
display: flex;
flex-direction: row;
@media screen and (max-width: 550px) {
margin-top: var(--spacing-card-xs);
}
}
.buttons {
display: flex;
flex-direction: column;
gap: var(--spacing-card-sm);
align-items: flex-end;
flex-grow: 1;
}
.small-mode {
@media screen and (min-width: 750px) {
grid-template:
'icon title'
'icon description'
'icon tags'
'stats stats' !important;
grid-template-columns: min-content auto !important;
grid-template-rows: min-content 1fr min-content min-content !important;
.tags {
margin-top: var(--spacing-card-xs) !important;
}
.stats {
flex-direction: row;
column-gap: var(--spacing-card-md) !important;
margin-top: var(--spacing-card-xs) !important;
.stat-label {
display: none !important;
}
}
}
}
</style>
@@ -9,13 +9,13 @@
<ButtonStyled color="brand">
<button class="brand-button" @click="acceptInvite()">
<CheckIcon />
{{ getFormattedMessage(messages.accept) }}
{{ getFormattedMessage(commonMessages.acceptButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="declineInvite">
<XIcon />
{{ getFormattedMessage(messages.decline) }}
{{ getFormattedMessage(commonMessages.declineButton) }}
</button>
</ButtonStyled>
</div>
@@ -26,6 +26,7 @@
import { CheckIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
type MessageDescriptor,
@@ -81,14 +82,6 @@ const messages = defineMessages({
defaultMessage:
"You've been invited to join this project. Please accept or decline the invitation.",
},
accept: {
id: 'project-member-header.accept',
defaultMessage: 'Accept',
},
decline: {
id: 'project-member-header.decline',
defaultMessage: 'Decline',
},
successJoin: {
id: 'project-member-header.success-join',
defaultMessage: 'You have joined the project team',
@@ -105,14 +98,6 @@ const messages = defineMessages({
id: 'project-member-header.error-decline',
defaultMessage: 'Failed to decline team invitation',
},
success: {
id: 'project-member-header.success',
defaultMessage: 'Success',
},
error: {
id: 'project-member-header.error',
defaultMessage: 'Error',
},
})
const { formatMessage } = useVIntl()
@@ -171,13 +156,13 @@ async function acceptInvite(): Promise<void> {
await acceptTeamInvite(props.project.team)
await handleUpdateMembers()
addNotification({
title: formatMessage(messages.success),
title: formatMessage(commonMessages.successLabel),
text: formatMessage(messages.successJoin),
type: 'success',
})
} catch {
addNotification({
title: formatMessage(messages.error),
title: formatMessage(commonMessages.errorLabel),
text: formatMessage(messages.errorJoin),
type: 'error',
})
@@ -192,13 +177,13 @@ async function declineInvite(): Promise<void> {
await removeTeamMember(props.project.team, props.auth.user.id)
await handleUpdateMembers()
addNotification({
title: formatMessage(messages.success),
title: formatMessage(commonMessages.successLabel),
text: formatMessage(messages.successDecline),
type: 'success',
})
} catch {
addNotification({
title: formatMessage(messages.error),
title: formatMessage(commonMessages.errorLabel),
text: formatMessage(messages.errorDecline),
type: 'error',
})
@@ -90,7 +90,7 @@
</span>
</label>
<div
class="text-muted flex flex-col gap-2 rounded-lg border border-divider bg-button-bg p-4"
class="text-muted flex flex-col gap-2 rounded-lg border border-surface-5 bg-button-bg p-4"
>
<span>Hi {user.name},</span>
<div class="textarea-wrapper">
@@ -46,20 +46,20 @@
Node hostnames
<span class="text-brand-red">*</span>
</span>
<span>Add nodes to transfer.</span>
<span>Add nodes to transfer (comma or space-separated).</span>
</label>
<div class="flex items-center gap-2">
<input
id="node-input"
v-model="nodeInput"
class="w-40"
class="w-64"
type="text"
autocomplete="off"
placeholder="us-vin200"
@keydown.enter.prevent="addNode"
placeholder="us-vin200, us-vin201"
@keydown.enter.prevent="addNodes"
/>
<ButtonStyled color="blue" color-fill="text">
<button class="shrink-0" @click="addNode">
<button class="shrink-0" @click="addNodes">
<PlusIcon />
Add
</button>
@@ -282,18 +282,37 @@ function hide() {
modal.value?.hide()
}
function addNode() {
const v = nodeInput.value.trim()
if (!v) return
if (!nodeHostnames.value.includes(v)) {
function addNodes() {
const input = nodeInput.value.trim()
if (!input) return
const nodes = input
.split(/[,\s]+/)
.map((s) => s.trim())
.filter((s) => s.length > 0)
const unknownNodes: string[] = []
const addedNodes: string[] = []
for (const v of nodes) {
if (!nodeHostnames.value.includes(v)) {
unknownNodes.push(v)
continue
}
if (!selectedNodes.value.includes(v)) {
selectedNodes.value.push(v)
addedNodes.push(v)
}
}
if (unknownNodes.length > 0) {
addNotification({
title: 'Unknown node',
text: "This hostname doesn't exist",
title: `Unknown node${unknownNodes.length > 1 ? 's' : ''}`,
text: unknownNodes.join(', '),
type: 'error',
})
return
}
if (!selectedNodes.value.includes(v)) selectedNodes.value.push(v)
nodeInput.value = ''
}
@@ -74,7 +74,7 @@
<div class="chart-controls">
<h2>
<span class="label__title">
{{ formatCategoryHeader(selectedChart) }}
{{ capitalizeString(selectedChart) }}
</span>
<span class="label__subtitle">
{{ formattedCategorySubtitle }}
@@ -311,7 +311,7 @@
<script setup lang="ts">
import { DownloadIcon, PaletteIcon, UpdatedIcon } from '@modrinth/assets'
import { Button, Card, DropdownSelect } from '@modrinth/ui'
import { formatCategoryHeader, formatMoney, formatNumber } from '@modrinth/utils'
import { capitalizeString, formatMoney, formatNumber } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed } from 'vue'
@@ -27,8 +27,8 @@
"
:style="`--_color: var(--color-platform-${loader.name})`"
>
<div v-html="loader.icon"></div>
{{ formatCategory(loader.name) }}
<component :is="getLoaderIcon(loader.name)" v-if="getLoaderIcon(loader.name)" />
<FormattedTag :tag="loader.name" enforce-type="loader" />
</TagItem>
</div>
</div>
@@ -40,8 +40,8 @@
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { Chips, TagItem } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils'
import { getLoaderIcon } from '@modrinth/assets'
import { Chips, FormattedTag, TagItem } from '@modrinth/ui'
const selectedLoaders = defineModel<string[]>({ default: [] })
@@ -155,7 +155,7 @@ function groupVersions(gameVersions: GameVersion[]) {
const groups: Record<string, string[]> = {}
let currentGroupKey = getSnapshotGroupKey(gameVersions.find((v) => v.major)?.version || '')
let currentGroupKey
gameVersions.forEach((gameVersion) => {
if (gameVersion.version_type === 'release') {
@@ -29,8 +29,8 @@
class="border !border-solid border-surface-5 !transition-all hover:bg-button-bgHover hover:no-underline"
:style="`--_color: var(--color-platform-${loader.name})`"
>
<div v-html="loader.icon"></div>
{{ formatCategory(loader.name) }}
<component :is="getLoaderIcon(loader.name)" v-if="getLoaderIcon(loader.name)" />
<FormattedTag :tag="loader.name" enforce-type="loader" />
<XIcon class="text-secondary" />
</TagItem>
</template>
@@ -41,9 +41,8 @@
</template>
<script lang="ts" setup>
import { XIcon } from '@modrinth/assets'
import { ButtonStyled, TagItem } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils'
import { getLoaderIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, FormattedTag, TagItem } from '@modrinth/ui'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
@@ -72,8 +72,8 @@
class="border !border-solid border-surface-5 hover:no-underline"
:style="`--_color: var(--color-platform-${loader.name})`"
>
<div v-html="loader.icon"></div>
{{ formatCategory(loader.name) }}
<component :is="getLoaderIcon(loader.name)" v-if="getLoaderIcon(loader.name)" />
<FormattedTag :tag="loader.name" enforce-type="loader" />
</TagItem>
</template>
@@ -189,16 +189,16 @@
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { EditIcon, UnknownIcon } from '@modrinth/assets'
import { EditIcon, getLoaderIcon, UnknownIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
ENVIRONMENTS_COPY,
FormattedTag,
injectProjectPageContext,
TagItem,
useVIntl,
} from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils'
import { useGeneratedState } from '~/composables/generated'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
@@ -43,7 +43,7 @@
<ButtonStyled class="w-24">
<button @click="modal.hide()">
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancel) }}
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand" class="w-36">
@@ -60,6 +60,7 @@
import { PlusIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
NewModal,
@@ -102,18 +103,10 @@ const messages = defineMessages({
defaultMessage:
'Your new collection will be created as a public collection with {count, plural, =0 {no projects} one {# project} other {# projects}}.',
},
cancel: {
id: 'create.collection.cancel',
defaultMessage: 'Cancel',
},
createCollection: {
id: 'create.collection.create-collection',
defaultMessage: 'Create collection',
},
errorTitle: {
id: 'create.collection.error-title',
defaultMessage: 'An error occurred',
},
})
const name = ref('')
@@ -150,7 +143,7 @@ async function create() {
await router.push(`/collection/${result.id}`)
} catch (err) {
addNotification({
title: formatMessage(messages.errorTitle),
title: formatMessage(commonMessages.errorNotificationTitle),
text: err?.data?.description || err?.message || err,
type: 'error',
})
@@ -65,7 +65,7 @@
<ButtonStyled class="w-24">
<button @click="hide">
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancel) }}
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand" class="w-40">
@@ -83,6 +83,7 @@
import { PlusIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
NewModal,
@@ -130,18 +131,10 @@ const messages = defineMessages({
defaultMessage:
'You will be the owner of this organization, but you can invite other members and transfer ownership at any time.',
},
cancel: {
id: 'create.organization.cancel',
defaultMessage: 'Cancel',
},
createOrganization: {
id: 'create.organization.create-organization',
defaultMessage: 'Create organization',
},
errorTitle: {
id: 'create.organization.error-title',
defaultMessage: 'An error occurred',
},
})
const name = ref<string>('')
@@ -172,7 +165,7 @@ async function createOrganization(): Promise<void> {
} catch (err: any) {
console.error(err)
addNotification({
title: formatMessage(messages.errorTitle),
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -43,7 +43,7 @@
<div class="flex flex-col gap-2">
<label for="visibility" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">
{{ formatMessage(messages.visibilityLabel) }}
{{ formatMessage(commonMessages.visibilityLabel) }}
<span class="text-brand-red">*</span>
</span>
<span>{{ formatMessage(messages.visibilityDescription) }}</span>
@@ -79,7 +79,7 @@
<ButtonStyled class="w-24">
<button @click="cancel">
<XIcon aria-hidden="true" />
{{ formatMessage(messages.cancel) }}
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand" class="w-32">
@@ -98,6 +98,7 @@ import { PlusIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Chips,
commonMessages,
defineMessages,
injectNotificationManager,
NewModal,
@@ -127,10 +128,6 @@ const messages = defineMessages({
id: 'create.project.url-label',
defaultMessage: 'URL',
},
visibilityLabel: {
id: 'create.project.visibility-label',
defaultMessage: 'Visibility',
},
visibilityDescription: {
id: 'create.project.visibility-description',
defaultMessage: 'The visibility of your project after it has been approved.',
@@ -147,18 +144,10 @@ const messages = defineMessages({
id: 'create.project.summary-placeholder',
defaultMessage: 'This project adds...',
},
cancel: {
id: 'create.project.cancel',
defaultMessage: 'Cancel',
},
createProject: {
id: 'create.project.create-project',
defaultMessage: 'Create project',
},
errorTitle: {
id: 'create.project.error-title',
defaultMessage: 'An error occurred',
},
visibilityPublic: {
id: 'create.project.visibility-public',
defaultMessage: 'Public',
@@ -256,7 +245,7 @@ async function createProject() {
await router.push(`/project/${slug.value}/settings`)
} catch (err) {
addNotification({
title: formatMessage(messages.errorTitle),
title: formatMessage(commonMessages.errorNotificationTitle),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -37,7 +37,10 @@
v-model="isUSCitizen"
:items="['yes', 'no']"
:format-label="
(item) => (item === 'yes' ? formatMessage(messages.yes) : formatMessage(messages.no))
(item) =>
item === 'yes'
? formatMessage(commonMessages.yesLabel)
: formatMessage(commonMessages.noLabel)
"
:never-empty="false"
:capitalize="true"
@@ -162,6 +165,7 @@ import {
Admonition,
ButtonStyled,
Chips,
commonMessages,
defineMessages,
injectNotificationManager,
IntlFormatted,
@@ -228,8 +232,6 @@ const messages = defineMessages({
id: 'dashboard.creator-tax-form-modal.us-citizen.question',
defaultMessage: 'Are you a US citizen?',
},
yes: { id: 'common.yes', defaultMessage: 'Yes' },
no: { id: 'common.no', defaultMessage: 'No' },
entityQuestion: {
id: 'dashboard.creator-tax-form-modal.entity.question',
defaultMessage: 'Are you a private individual or part of a foreign entity?',
@@ -60,7 +60,7 @@
<div v-if="currentStage === 'completion'" class="mt-4 flex w-full gap-3">
<ButtonStyled class="flex-1">
<button class="w-full text-contrast" @click="handleClose">
{{ formatMessage(messages.closeButton) }}
{{ formatMessage(commonMessages.closeButton) }}
</button>
</ButtonStyled>
<ButtonStyled class="flex-1">
@@ -601,26 +601,10 @@ const messages = defineMessages({
id: 'dashboard.creator-withdraw-modal.stage.method-selection',
defaultMessage: 'Method',
},
tremendousDetailsStage: {
id: 'dashboard.creator-withdraw-modal.stage.tremendous-details',
defaultMessage: 'Details',
},
muralpayKycStage: {
id: 'dashboard.creator-withdraw-modal.stage.muralpay-kyc',
defaultMessage: 'Verification',
},
muralpayDetailsStage: {
id: 'dashboard.creator-withdraw-modal.stage.muralpay-details',
defaultMessage: 'Account Details',
},
completionStage: {
id: 'dashboard.creator-withdraw-modal.stage.completion',
defaultMessage: 'Complete',
},
detailsLabel: {
id: 'dashboard.creator-withdraw-modal.details-label',
defaultMessage: 'Details',
},
completeTaxForm: {
id: 'dashboard.creator-withdraw-modal.complete-tax-form',
defaultMessage: 'Complete tax form',
@@ -629,14 +613,14 @@ const messages = defineMessages({
id: 'dashboard.creator-withdraw-modal.continue-with-limit',
defaultMessage: 'Continue with limit',
},
detailsLabel: {
id: 'dashboard.creator-withdraw-modal.details-label',
defaultMessage: 'Details',
},
withdrawButton: {
id: 'dashboard.creator-withdraw-modal.withdraw-button',
defaultMessage: 'Withdraw',
},
closeButton: {
id: 'dashboard.withdraw.completion.close-button',
defaultMessage: 'Close',
},
transactionsButton: {
id: 'dashboard.withdraw.completion.transactions-button',
defaultMessage: 'Transactions',
@@ -68,7 +68,7 @@
{{ formatMoney(result?.fee || 0) }}
</span>
</div>
<div class="border-b-1 h-0 w-full rounded-full border-b border-solid border-divider" />
<div class="border-b-1 h-0 w-full rounded-full border-b border-solid border-surface-5" />
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
@@ -287,13 +287,5 @@ const messages = defineMessages({
defaultMessage:
"You'll receive an email at <b>{email}</b> with instructions to redeem your withdrawal.",
},
closeButton: {
id: 'dashboard.withdraw.completion.close-button',
defaultMessage: 'Close',
},
transactionsButton: {
id: 'dashboard.withdraw.completion.transactions-button',
defaultMessage: 'Transactions',
},
})
</script>
@@ -57,8 +57,8 @@
<SaveIcon v-else />
{{
venmoSaveSuccess
? formatMessage(messages.savedButton)
: formatMessage(messages.saveButton)
? formatMessage(commonMessages.savedLabel)
: formatMessage(commonMessages.saveButton)
}}
</button>
</ButtonStyled>
@@ -107,6 +107,7 @@ import { CheckIcon, PayPalColorIcon, SaveIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
commonMessages,
defineMessages,
financialMessages,
formFieldLabels,
@@ -342,14 +343,6 @@ const messages = defineMessages({
id: 'dashboard.creator-withdraw-modal.paypal-details.disconnect-account',
defaultMessage: 'Disconnect account',
},
saveButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.save-button',
defaultMessage: 'Save',
},
savedButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.saved-button',
defaultMessage: 'Saved',
},
saveSuccess: {
id: 'dashboard.creator-withdraw-modal.paypal-details.save-success',
defaultMessage: 'Venmo handle saved successfully!',
@@ -90,66 +90,41 @@
</Combobox>
</div>
<span v-if="selectedMethodDetails" class="text-secondary">
{{
formatMoney(
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
? (fixedDenominationMin ?? effectiveMinAmount) / selectedMethodExchangeRate
: (fixedDenominationMin ?? effectiveMinAmount),
)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
({{
{{ formatMoney(displayMinUsd)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'"
>({{
formatAmountForDisplay(
fixedDenominationMin ?? effectiveMinAmount,
displayMinLocal,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
)
}})</template
>
min,
{{
formatMoney(
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
? (fixedDenominationMax ??
selectedMethodDetails.interval?.standard?.max ??
effectiveMaxAmount) / selectedMethodExchangeRate
: (fixedDenominationMax ??
selectedMethodDetails.interval?.standard?.max ??
effectiveMaxAmount),
)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
({{
formatAmountForDisplay(
fixedDenominationMax ??
selectedMethodDetails.interval?.standard?.max ??
effectiveMaxAmount,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
)
}})</template
min<template v-if="displayMinUsd <= roundedMaxAmount"
>, {{ formatMoney(displayMaxUsd)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'"
>({{
formatAmountForDisplay(
displayMaxLocal,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
)
}})</template
>
max</template
>
max withdrawal amount.
withdrawal amount.
</span>
<span
v-if="selectedMethodDetails && effectiveMinAmount > roundedMaxAmount"
class="text-sm text-red"
>
You need at least
{{
formatMoney(
selectedMethodCurrencyCode &&
selectedMethodCurrencyCode !== 'USD' &&
selectedMethodExchangeRate
? effectiveMinAmount / selectedMethodExchangeRate
: effectiveMinAmount,
)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
({{
{{ formatMoney(displayMinUsd)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'"
>({{
formatAmountForDisplay(
effectiveMinAmount,
displayMinLocal,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
)
@@ -307,7 +282,19 @@
v-if="!useDenominationSuggestions && denominationOptions.length === 0"
class="text-error text-sm"
>
No denominations available for your current balance
<template v-if="rawFixedDenominationMin !== null">
The minimum denomination is
{{
formatAmountForDisplay(
rawFixedDenominationMin,
selectedMethodCurrencyCode,
selectedMethodExchangeRate,
)
}}<template v-if="selectedMethodCurrencyCode && selectedMethodCurrencyCode !== 'USD'">
({{ formatMoney(convertToUsd(rawFixedDenominationMin)) }})</template
>, which exceeds your balance of {{ formatMoney(roundedMaxAmount) }}.
</template>
<template v-else>No denominations available for your current balance</template>
</span>
</div>
@@ -324,7 +311,7 @@
<WithdrawFeeBreakdown
v-if="allRequiredFieldsFilled && formData.amount && formData.amount > 0"
:amount="formData.amount || 0"
:amount="amountForFeeBreakdown"
:fee="calculatedFee"
:fee-loading="feeLoading"
:exchange-rate="showGiftCardSelector ? selectedMethodExchangeRate : giftCardExchangeRate"
@@ -720,6 +707,34 @@ const hasSelectedDenomination = computed(() => {
)
})
// Convert local currency amount to USD using the selected method's exchange rate
const convertToUsd = (localAmount: number): number => {
const exchangeRate = selectedMethodExchangeRate.value
if (
selectedMethodCurrencyCode.value &&
selectedMethodCurrencyCode.value !== 'USD' &&
exchangeRate &&
exchangeRate > 0
) {
return localAmount / exchangeRate
}
return localAmount
}
// Convert USD amount to local currency using the selected method's exchange rate
const convertToLocalCurrency = (usdAmount: number): number => {
const exchangeRate = selectedMethodExchangeRate.value
if (
selectedMethodCurrencyCode.value &&
selectedMethodCurrencyCode.value !== 'USD' &&
exchangeRate &&
exchangeRate > 0
) {
return usdAmount * exchangeRate
}
return usdAmount
}
const denominationOptions = computed(() => {
const interval = selectedMethodDetails.value?.interval
if (!interval) return []
@@ -735,30 +750,56 @@ const denominationOptions = computed(() => {
if (values.length === 0) return []
const filtered = values.filter((amount) => amount <= roundedMaxAmount.value).sort((a, b) => a - b)
// Convert USD balance to local currency for comparison with denomination values
// (denomination values are in local currency, e.g., 50 INR, 45000 IDR)
const maxInLocalCurrency = convertToLocalCurrency(roundedMaxAmount.value)
const filtered = values.filter((amount) => amount <= maxInLocalCurrency).sort((a, b) => a - b)
debug(
'Denomination options (filtered by max):',
filtered,
'from',
values,
'max:',
'max (local currency):',
maxInLocalCurrency,
'max (USD):',
roundedMaxAmount.value,
)
return filtered
})
const effectiveMinAmount = computed(() => {
return selectedMethodDetails.value?.interval?.standard?.min || 0.01
const min = selectedMethodDetails.value?.interval?.standard?.min || 0.01
// Convert from local currency to USD for display/validation
return convertToUsd(min)
})
const effectiveMaxAmount = computed(() => {
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
if (methodMax !== undefined && methodMax !== null) {
return Math.min(roundedMaxAmount.value, methodMax)
// Convert method max from local currency to USD, then compare with USD balance
const methodMaxInUsd = convertToUsd(methodMax)
return Math.min(roundedMaxAmount.value, methodMaxInUsd)
}
return roundedMaxAmount.value
})
// Get the minimum fixed denomination from the full list (not filtered by user balance)
const rawFixedDenominationMin = computed(() => {
const interval = selectedMethodDetails.value?.interval
if (!interval) return null
let values: number[] = []
if (interval.fixed?.values) {
values = [...interval.fixed.values]
} else if (interval.standard && interval.standard.min === interval.standard.max) {
values = [interval.standard.min]
}
if (values.length === 0) return null
return Math.min(...values)
})
const fixedDenominationMin = computed(() => {
if (!useFixedDenominations.value) return null
const options = denominationOptions.value
@@ -773,6 +814,63 @@ const fixedDenominationMax = computed(() => {
return options[options.length - 1]
})
// - Fixed denominations: convert from local currency to USD
// - Variable amounts: effectiveMinAmount/effectiveMaxAmount are already in USD
const displayMinUsd = computed(() => {
if (fixedDenominationMin.value !== null) {
// Fixed denomination is in local currency, convert to USD
return convertToUsd(fixedDenominationMin.value)
}
// If no affordable denominations but there are fixed values, show the raw minimum
if (rawFixedDenominationMin.value !== null) {
return convertToUsd(rawFixedDenominationMin.value)
}
// effectiveMinAmount is already in USD
return effectiveMinAmount.value
})
const displayMaxUsd = computed(() => {
if (fixedDenominationMax.value !== null) {
// Fixed denomination is in local currency, convert to USD
return convertToUsd(fixedDenominationMax.value)
}
// For variable amounts, use effectiveMaxAmount (already in USD)
// But also check if there's a method max from the interval
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
if (methodMax !== undefined && methodMax !== null) {
const methodMaxUsd = convertToUsd(methodMax)
return Math.min(effectiveMaxAmount.value, methodMaxUsd)
}
return effectiveMaxAmount.value
})
// Display values in local currency (for showing in parentheses)
const displayMinLocal = computed(() => {
if (fixedDenominationMin.value !== null) {
// Fixed denomination is already in local currency
return fixedDenominationMin.value
}
// If no affordable denominations but there are fixed values, show the raw minimum
if (rawFixedDenominationMin.value !== null) {
return rawFixedDenominationMin.value
}
// Convert USD to local currency
return convertToLocalCurrency(effectiveMinAmount.value)
})
const displayMaxLocal = computed(() => {
if (fixedDenominationMax.value !== null) {
// Fixed denomination is already in local currency
return fixedDenominationMax.value
}
// Check for method max
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
if (methodMax !== undefined && methodMax !== null) {
return Math.min(convertToLocalCurrency(effectiveMaxAmount.value), methodMax)
}
return convertToLocalCurrency(effectiveMaxAmount.value)
})
const selectedDenomination = computed({
get: () => formData.value.amount,
set: (value) => {
@@ -791,6 +889,21 @@ const allRequiredFieldsFilled = computed(() => {
return true
})
// Amount to display in WithdrawFeeBreakdown (expects local currency for gift cards)
const amountForFeeBreakdown = computed(() => {
const amount = formData.value.amount ?? 0
if (!showGiftCardSelector.value) {
// Non-gift-card: amount is in USD
return amount
}
if (useFixedDenominations.value) {
// Fixed denominations: amount is already in local currency
return amount
}
// Variable amount gift card: amount is in USD, convert to local currency
return convertToLocalCurrency(amount)
})
const calculateFeesDebounced = useDebounceFn(async () => {
const amount = formData.value.amount
if (!amount || amount <= 0) {
@@ -852,7 +965,15 @@ watch(
watch(
[() => formData.value.amount, selectedGiftCardId, deliveryEmail, selectedCurrency],
() => {
withdrawData.value.calculation.amount = formData.value.amount ?? 0
let amountForBackend = formData.value.amount ?? 0
// - Fixed denominations (chips): formData.amount is already in local currency
// - Variable amounts (RevenueInputField): formData.amount is in USD, needs conversion
if (showGiftCardSelector.value && !useFixedDenominations.value && amountForBackend > 0) {
amountForBackend = convertToLocalCurrency(amountForBackend)
}
withdrawData.value.calculation.amount = amountForBackend
if (showGiftCardSelector.value && selectedGiftCardId.value) {
withdrawData.value.selection.methodId = selectedGiftCardId.value
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ClipboardCopyIcon, DownloadIcon, LoaderCircleIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, CopyCode, NewModal } from '@modrinth/ui'
import { ButtonStyled, CopyCode, NewModal, useDebugLogger } from '@modrinth/ui'
import { ref, useTemplateRef } from 'vue'
export type UnsafeFile = {
@@ -16,6 +16,8 @@ const props = defineProps<{
unsafeFiles: UnsafeFile[]
}>()
const debug = useDebugLogger('MaliciousSummaryModal')
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
const versionDataCache = ref<
@@ -36,7 +38,7 @@ async function fetchVersionHashes(versionIds: string[]) {
versionDataCache.value.set(versionId, { files: new Map(), loading: true })
try {
// TODO: switch to api-client once truman's vers stuff is merged
const version = (await useBaseFetch(`version/${versionId}`)) as {
const version = (await useBaseFetch(`version/${versionId}`, { apiVersion: 3 })) as {
files: Array<{
id?: string
filename: string
@@ -44,6 +46,11 @@ async function fetchVersionHashes(versionIds: string[]) {
}>
}
const filesMap = new Map<string, string>()
debug('Full version response:', version)
debug(
'Version files:',
version.files.map((f) => ({ id: f.id, filename: f.filename })),
)
for (const file of version.files) {
if (file.id) {
filesMap.set(file.id, file.hashes.sha512)
@@ -62,7 +69,9 @@ async function fetchVersionHashes(versionIds: string[]) {
}
function getFileHash(versionId: string, fileId: string): string | undefined {
return versionDataCache.value.get(versionId)?.files.get(fileId)
const hash = versionDataCache.value.get(versionId)?.files.get(fileId)
debug('getFileHash:', { versionId, fileId, found: !!hash })
return hash
}
function isHashLoading(versionId: string): boolean {
@@ -78,6 +78,7 @@
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
AsteriskIcon,
ChevronRightIcon,
@@ -90,7 +91,6 @@ import {
import type { Nag, NagContext, NagStatus } from '@modrinth/moderation'
import { nags } from '@modrinth/moderation'
import { ButtonStyled, defineMessages, type MessageDescriptor, useVIntl } from '@modrinth/ui'
import type { Project, User, Version } from '@modrinth/utils'
import type { Component } from 'vue'
import { computed } from 'vue'
@@ -98,16 +98,10 @@ interface Tags {
rejectedStatuses: string[]
}
interface Member {
accepted?: boolean
project_role?: string
user?: Partial<User>
}
interface Props {
project: Project
versions?: Version[]
currentMember?: Member | null
project: Labrinth.Projects.v2.Project
versions?: Labrinth.Versions.v2.Version[]
currentMember?: Labrinth.Projects.v3.TeamMember | null
collapsed?: boolean
routeName?: string
tags: Tags
@@ -179,7 +173,7 @@ const emit = defineEmits<{
const nagContext = computed<NagContext>(() => ({
project: props.project,
versions: props.versions,
currentMember: props.currentMember as User,
currentMember: props.currentMember?.user as Labrinth.Users.v2.User,
currentRoute: props.routeName,
tags: props.tags,
submitProject: submitForReview,
@@ -2,6 +2,7 @@
import type { Labrinth } from '@modrinth/api-client'
import {
BugIcon,
CheckCircleIcon,
CheckIcon,
ChevronDownIcon,
ClipboardCopyIcon,
@@ -13,6 +14,7 @@ import {
LoaderCircleIcon,
ShieldCheckIcon,
TimerIcon,
TriangleAlertIcon,
} from '@modrinth/assets'
import { type TechReviewContext, techReviewQuickReplies } from '@modrinth/moderation'
import {
@@ -34,7 +36,7 @@ import {
type User,
} from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, ref, watch } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
import type { UnsafeFile } from '~/components/ui/moderation/MaliciousSummaryModal.vue'
import NavTabs from '~/components/ui/NavTabs.vue'
@@ -159,8 +161,8 @@ const client = injectModrinthClient()
const severityOrder = { severe: 3, high: 2, medium: 1, low: 0 } as Record<string, number>
const detailDecisions = ref<Map<string, 'safe' | 'malware'>>(new Map())
const updatingDetails = ref<Set<string>>(new Set())
const detailDecisions = reactive<Map<string, 'safe' | 'malware'>>(new Map())
const updatingDetails = reactive<Set<string>>(new Set())
function getFileHighestSeverity(
file: FlattenedFileReport,
@@ -252,6 +254,16 @@ function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeve
}
}
function truncateMiddle(str: string, maxLength: number = 120): string {
if (str.length <= maxLength) return str
const separator = '...'
const sepLen = separator.length
const charsToShow = maxLength - sepLen
const frontChars = Math.ceil(charsToShow / 3)
const backChars = Math.floor((charsToShow * 2) / 3)
return str.slice(0, frontChars) + separator + str.slice(-backChars)
}
const severityColor = computed(() => {
switch (highestSeverity.value) {
case 'severe':
@@ -305,9 +317,9 @@ function backToFileList() {
async function copyToClipboard(code: string, detailId: string) {
try {
await navigator.clipboard.writeText(code)
showCopyFeedback.value.set(detailId, true)
showCopyFeedback.set(detailId, true)
setTimeout(() => {
showCopyFeedback.value.delete(detailId)
showCopyFeedback.delete(detailId)
}, 2000)
} catch (error) {
console.error('Failed to copy code:', error)
@@ -318,7 +330,7 @@ function getDetailDecision(
detailId: string,
backendStatus: Labrinth.TechReview.Internal.DelphiReportIssueStatus,
): 'safe' | 'malware' | 'pending' {
const localDecision = detailDecisions.value.get(detailId)
const localDecision = detailDecisions.get(detailId)
if (localDecision) return localDecision
if (backendStatus === 'safe') return 'safe'
if (backendStatus === 'unsafe') return 'malware'
@@ -329,9 +341,7 @@ function isPreReviewed(
detailId: string,
backendStatus: Labrinth.TechReview.Internal.DelphiReportIssueStatus,
): boolean {
return (
(backendStatus === 'safe' || backendStatus === 'unsafe') && !detailDecisions.value.has(detailId)
)
return (backendStatus === 'safe' || backendStatus === 'unsafe') && !detailDecisions.has(detailId)
}
function getMarkedFlagsCount(flags: ClassGroup['flags']): number {
@@ -357,8 +367,82 @@ function getFileMarkedCount(file: FlattenedFileReport): number {
return count
}
const remainingUnmarkedCount = computed(() => {
if (!selectedFile.value) return 0
return getFileDetailCount(selectedFile.value) - getFileMarkedCount(selectedFile.value)
})
const isBatchUpdating = ref(false)
async function batchMarkRemaining(verdict: 'safe' | 'unsafe') {
if (!selectedFile.value || isBatchUpdating.value) return
const detailIds: string[] = []
for (const issue of selectedFile.value.issues) {
for (const detail of issue.details) {
const detailWithStatus = detail as typeof detail & {
status: Labrinth.TechReview.Internal.DelphiReportIssueStatus
}
if (getDetailDecision(detailWithStatus.id, detailWithStatus.status) === 'pending') {
detailIds.push(detail.id)
}
}
}
if (detailIds.length === 0) return
isBatchUpdating.value = true
try {
await Promise.all(
detailIds.map((detailId) =>
client.labrinth.tech_review_internal.updateIssueDetail(detailId, { verdict }),
),
)
const decision = verdict === 'safe' ? 'safe' : 'malware'
for (const detailId of detailIds) {
detailDecisions.set(detailId, decision)
}
addNotification({
type: 'success',
title: `Marked ${detailIds.length} traces as ${verdict}`,
text: `All remaining traces have been marked as ${verdict === 'safe' ? 'false positives' : 'malicious'}.`,
})
// Jump back to Files tab when all flags in the current file are marked
if (selectedFile.value) {
const markedCount = getFileMarkedCount(selectedFile.value)
const totalCount = getFileDetailCount(selectedFile.value)
if (markedCount === totalCount) {
backToFileList()
}
}
} catch (error) {
console.error('Failed to batch update:', error)
addNotification({
type: 'error',
title: 'Batch update failed',
text: 'An error occurred while updating traces.',
})
} finally {
isBatchUpdating.value = false
}
}
async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe') {
updatingDetails.value.add(detailId)
let priorDecision: 'safe' | 'malware' | 'pending' = 'pending'
outer: for (const report of props.item.reports) {
for (const issue of report.issues) {
const detail = issue.details.find((d) => d.id === detailId)
if (detail) {
priorDecision = getDetailDecision(detail.id, detail.status)
break outer
}
}
}
updatingDetails.add(detailId)
try {
await client.labrinth.tech_review_internal.updateIssueDetail(detailId, { verdict })
@@ -383,7 +467,7 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
for (const issue of report.issues) {
for (const detail of issue.details) {
if (detail.key === detailKey) {
detailDecisions.value.set(detail.id, decision)
detailDecisions.set(detail.id, decision)
if (detail.id !== detailId) {
otherMatchedCount++
}
@@ -392,14 +476,17 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
}
}
} else {
detailDecisions.value.set(detailId, decision)
detailDecisions.set(detailId, decision)
}
for (const classGroup of groupedByClass.value) {
const hasThisDetail = classGroup.flags.some((f) => f.detail.id === detailId)
if (hasThisDetail && getMarkedFlagsCount(classGroup.flags) === classGroup.flags.length) {
expandedClasses.value.delete(classGroup.filePath)
break
// Only collapse if the prior state was 'pending' (new decision, not updating existing)
if (priorDecision === 'pending') {
for (const classGroup of groupedByClass.value) {
const hasThisDetail = classGroup.flags.some((f) => f.detail.id === detailId)
if (hasThisDetail && getMarkedFlagsCount(classGroup.flags) === classGroup.flags.length) {
expandedClasses.delete(classGroup.filePath)
break
}
}
}
@@ -438,12 +525,12 @@ async function updateDetailStatus(detailId: string, verdict: 'safe' | 'unsafe')
text: 'An error occurred while updating the issue status.',
})
} finally {
updatingDetails.value.delete(detailId)
updatingDetails.delete(detailId)
}
}
const expandedClasses = ref<Set<string>>(new Set())
const showCopyFeedback = ref<Map<string, boolean>>(new Map())
const expandedClasses = reactive<Set<string>>(new Set())
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
interface ClassGroup {
filePath: string
@@ -503,7 +590,7 @@ watch(
groupedByClass,
(classes) => {
if (classes.length === 1) {
expandedClasses.value.add(classes[0].filePath)
expandedClasses.add(classes[0].filePath)
}
},
{ immediate: true },
@@ -522,10 +609,10 @@ function getHighestSeverityInClass(
}
function toggleClass(filePath: string) {
if (expandedClasses.value.has(filePath)) {
expandedClasses.value.delete(filePath)
if (expandedClasses.has(filePath)) {
expandedClasses.delete(filePath)
} else {
expandedClasses.value.add(filePath)
expandedClasses.add(filePath)
}
}
@@ -621,6 +708,7 @@ const reviewSummaryPreview = computed(() => {
const timestamp = dayjs().utc().format('MMMM D, YYYY [at] h:mm A [UTC]')
let markdown = `## Tech Review Summary\n*${timestamp}*\n\n`
markdown += `<details>\n<summary>File Details (${totalSafe} safe, ${totalUnsafe} unsafe)</summary>\n\n`
for (const [, fileData] of fileDecisions) {
if (fileData.decisions.length === 0) continue
@@ -643,6 +731,7 @@ const reviewSummaryPreview = computed(() => {
markdown += `\n</details>\n\n`
}
markdown += `</details>\n\n`
markdown += `---\n\n**Total:** ${totalDecisions} issues reviewed (${totalSafe} safe, ${totalUnsafe} unsafe)\n\n`
return markdown
@@ -919,11 +1008,12 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
>
<div class="flex items-center gap-3">
<span
v-tooltip="file.file_name"
class="font-medium text-contrast"
:class="{ 'cursor-pointer hover:underline': getFileDetailCount(file) > 0 }"
@click="getFileDetailCount(file) > 0 && viewFileFlags(file)"
>
{{ file.file_name }}
{{ truncateMiddle(file.file_name, 50) }}
</span>
<div class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1">
<span class="text-sm font-medium text-secondary">{{
@@ -989,11 +1079,31 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
</template>
<template v-else-if="currentTab === 'File' && selectedFile">
<div
v-if="remainingUnmarkedCount > 0"
class="flex gap-2 border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2 p-4"
>
<ButtonStyled color="brand" :disabled="isBatchUpdating">
<button @click="batchMarkRemaining('safe')">
<CheckCircleIcon class="size-5" />
Remaining safe ({{ remainingUnmarkedCount }})
</button>
</ButtonStyled>
<ButtonStyled color="red" :disabled="isBatchUpdating">
<button @click="batchMarkRemaining('unsafe')">
<TriangleAlertIcon class="size-5" />
Remaining malware ({{ remainingUnmarkedCount }})
</button>
</ButtonStyled>
</div>
<div
v-for="(classItem, idx) in groupedByClass"
:key="classItem.filePath"
class="border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2"
:class="{ 'rounded-bl-2xl rounded-br-2xl': idx === groupedByClass.length - 1 }"
:class="{
'rounded-bl-2xl rounded-br-2xl':
idx === groupedByClass.length - 1 && remainingUnmarkedCount === 0,
}"
>
<div
class="flex cursor-pointer items-center justify-between p-4 transition-colors duration-200 hover:bg-surface-4"
@@ -1009,7 +1119,9 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
</button>
</ButtonStyled>
<span class="font-mono font-semibold">{{ classItem.filePath }}</span>
<span v-tooltip="classItem.filePath" class="font-mono font-semibold">{{
truncateMiddle(classItem.filePath)
}}</span>
<div
class="rounded-full border-solid px-2.5 py-1"
@@ -1054,66 +1166,87 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
<div
v-for="flag in classItem.flags"
:key="`${flag.issueId}-${flag.detail.id}`"
class="grid grid-cols-[1fr_auto_auto] items-center rounded-lg border-[1px] border-b border-solid border-surface-5 bg-surface-3 py-2 pl-4 last:border-b-0"
class="flex flex-col gap-2 rounded-lg border-[1px] border-b border-solid border-surface-5 bg-surface-3 py-2 pl-4 last:border-b-0"
>
<span
class="text-base font-semibold text-contrast"
:class="{
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
}"
>{{ flag.issueType.replace(/_/g, ' ') }}</span
>
<div
class="flex w-20 justify-center"
:class="{
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
}"
>
<div class="grid grid-cols-[1fr_auto] items-center">
<div
class="rounded-full border-solid px-2.5 py-1"
:class="getSeverityBadgeColor(flag.detail.severity)"
class="flex items-center gap-2"
:class="{
'opacity-50': isPreReviewed(flag.detail.id, flag.detail.status),
}"
>
<span class="text-sm font-medium">{{
capitalizeString(flag.detail.severity)
<span class="text-base font-semibold text-contrast">{{
flag.issueType.replace(/_/g, ' ')
}}</span>
<div
class="rounded-full border-solid px-2.5 py-1"
:class="getSeverityBadgeColor(flag.detail.severity)"
>
<span class="text-sm font-medium">{{
capitalizeString(flag.detail.severity)
}}</span>
</div>
</div>
<div class="flex w-40 items-center justify-center gap-2">
<ButtonStyled
color="brand"
:type="
getDetailDecision(flag.detail.id, flag.detail.status) === 'safe'
? undefined
: 'outlined'
"
>
<button
class="!border-[1px]"
:disabled="updatingDetails.has(flag.detail.id)"
@click="updateDetailStatus(flag.detail.id, 'safe')"
>
Pass
</button>
</ButtonStyled>
<ButtonStyled
color="red"
:type="
getDetailDecision(flag.detail.id, flag.detail.status) === 'malware'
? undefined
: 'outlined'
"
>
<button
class="!border-[1px]"
:disabled="updatingDetails.has(flag.detail.id)"
@click="updateDetailStatus(flag.detail.id, 'unsafe')"
>
Fail
</button>
</ButtonStyled>
</div>
</div>
<div class="flex w-40 items-center justify-center gap-2">
<ButtonStyled
color="brand"
:type="
getDetailDecision(flag.detail.id, flag.detail.status) === 'safe'
? undefined
: 'outlined'
"
<div
v-if="flag.detail.data && Object.keys(flag.detail.data).length > 0"
class="flex flex-wrap gap-x-4 gap-y-1 pr-4 text-sm"
>
<div
v-for="[key, value] in Object.entries(flag.detail.data).sort(([a], [b]) =>
a.localeCompare(b),
)"
:key="key"
class="flex items-center gap-1.5"
>
<button
class="!border-[1px]"
:disabled="updatingDetails.has(flag.detail.id)"
@click="updateDetailStatus(flag.detail.id, 'safe')"
<span class="text-secondary">{{ key }}:</span>
<a
v-if="typeof value === 'string' && value.startsWith('http')"
:href="value"
target="_blank"
rel="noopener noreferrer"
class="text-brand-blue hover:underline"
>
Pass
</button>
</ButtonStyled>
<ButtonStyled
color="red"
:type="
getDetailDecision(flag.detail.id, flag.detail.status) === 'malware'
? undefined
: 'outlined'
"
>
<button
class="!border-[1px]"
:disabled="updatingDetails.has(flag.detail.id)"
@click="updateDetailStatus(flag.detail.id, 'unsafe')"
>
Fail
</button>
</ButtonStyled>
{{ value }}
</a>
<span v-else class="font-mono text-contrast">{{ value }}</span>
</div>
</div>
</div>
@@ -57,7 +57,7 @@
</div>
<div class="mt-auto">
<div
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-surface-5 pt-4"
>
<div class="flex items-center gap-2">
<ButtonStyled v-if="lockStatus.expired" @click="retryAcquireLock">
@@ -90,7 +90,7 @@
</div>
<div class="mt-auto">
<div
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-surface-5 pt-4"
>
<div class="flex items-center gap-2">
<ButtonStyled @click="reviewAnyway">
@@ -153,7 +153,7 @@
v-else
v-model="message"
type="text"
class="bg-bg-input h-[400px] w-full rounded-lg border border-solid border-divider px-3 py-2 font-mono text-base"
class="bg-bg-input h-[400px] w-full rounded-lg border border-solid border-surface-5 px-3 py-2 font-mono text-base"
placeholder="No message generated."
autocomplete="off"
@input="persistState"
@@ -317,7 +317,7 @@
<!-- Stage control buttons -->
<div class="mt-auto">
<div
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-surface-5 pt-4"
>
<div class="flex items-center gap-2">
<ButtonStyled v-if="!done && !generatedMessage && moderationStore.hasItems">
@@ -692,17 +692,23 @@ const emit = defineEmits<{
async function handleExit() {
// Release if we own the lock, or if there was an error checking (we might still own it)
if (lockStatus.value?.isOwnLock || lockError.value) {
const released = await moderationStore.releaseLock(projectV2.value.id)
const projectId = projectV2.value?.id
if (projectId && (lockStatus.value?.isOwnLock || lockError.value)) {
const released = await moderationStore.releaseLock(projectId)
if (!released && lockStatus.value?.isOwnLock) {
console.warn('Failed to release moderation lock for project:', projectV2.value.id)
console.warn('Failed to release moderation lock for project:', projectId)
}
}
emit('exit')
}
async function retryAcquireLock() {
const result = await moderationStore.acquireLock(projectV2.value.id)
const projectId = projectV2.value?.id
if (!projectId) {
console.warn('[retryAcquireLock] No project ID available')
return
}
const result = await moderationStore.acquireLock(projectId)
if (result.success) {
handleLockAcquired()
@@ -790,6 +796,8 @@ async function maintainPrefetchQueue() {
if (isPrefetching.value) return
if (!moderationStore.isQueueMode) return
const currentProjectId = projectV2.value?.id
isPrefetching.value = true
try {
@@ -798,7 +806,9 @@ async function maintainPrefetchQueue() {
prefetchQueue.value = prefetchQueue.value.filter((p) => now - p.validatedAt < PREFETCH_STALE_MS)
// 2. Remove entries for current project
prefetchQueue.value = prefetchQueue.value.filter((p) => p.projectId !== projectV2.value.id)
if (currentProjectId) {
prefetchQueue.value = prefetchQueue.value.filter((p) => p.projectId !== currentProjectId)
}
// 3. If queue is full enough, exit early
if (prefetchQueue.value.length >= PREFETCH_TARGET_COUNT) {
@@ -808,7 +818,7 @@ async function maintainPrefetchQueue() {
// 4. Get remaining queue items (excluding current and already prefetched)
const prefetchedIds = new Set(prefetchQueue.value.map((p) => p.projectId))
const queueItems = [...moderationStore.currentQueue.items]
const currentIndex = queueItems.indexOf(projectV2.value.id)
const currentIndex = currentProjectId ? queueItems.indexOf(currentProjectId) : -1
const remainingItems =
currentIndex >= 0 ? queueItems.slice(currentIndex + 1) : queueItems.slice(1)
@@ -873,7 +883,11 @@ const MAX_SKIP_ATTEMPTS = 10
async function skipToNextProject() {
// Skip the current project
const currentProjectId = projectV2.value.id
const currentProjectId = projectV2.value?.id
if (!currentProjectId) {
console.warn('[skipToNextProject] No current project ID, aborting')
return
}
debug('[skipToNextProject] Starting. Current project:', currentProjectId)
debug('[skipToNextProject] Queue before complete:', [...moderationStore.currentQueue.items])
@@ -1822,16 +1836,30 @@ function generateModpackMessage(allFiles: {
const hasNextProject = ref(false)
async function sendMessage(status: ProjectStatus) {
// Capture project data upfront to avoid null issues during async operations
const projectId = projectV2.value?.id
const threadId = projectV2.value?.thread_id
const projectType = projectV2.value?.project_type
if (!projectId) {
addNotification({
title: 'Error submitting moderation',
text: 'Project data unavailable. Please try again.',
type: 'error',
})
return
}
try {
await useBaseFetch(`project/${projectV2.value.id}`, {
await useBaseFetch(`project/${projectId}`, {
method: 'PATCH',
body: {
status,
},
})
if (message.value) {
await useBaseFetch(`thread/${projectV2.value.thread_id}`, {
if (message.value && threadId) {
await useBaseFetch(`thread/${threadId}`, {
method: 'POST',
body: {
body: {
@@ -1842,10 +1870,7 @@ async function sendMessage(status: ProjectStatus) {
})
}
if (
projectV2.value.project_type === 'modpack' &&
Object.keys(modpackJudgements.value).length > 0
) {
if (projectType === 'modpack' && Object.keys(modpackJudgements.value).length > 0) {
await useBaseFetch(`moderation/project`, {
internal: true,
method: 'POST',
@@ -1853,12 +1878,16 @@ async function sendMessage(status: ProjectStatus) {
})
}
const willHaveNext = moderationStore.completeCurrentProject(projectId, 'completed')
moderationStore.releaseLock(projectId).catch((err) => {
console.warn('Failed to release lock:', err)
})
// Set both states together - hasNextProject MUST be set before done
// to avoid the race condition where done=true renders with hasNextProject=false
hasNextProject.value = willHaveNext
done.value = true
// Release the lock after successful submission
await moderationStore.releaseLock(projectV2.value.id)
hasNextProject.value = moderationStore.completeCurrentProject(projectV2.value.id, 'completed')
} catch (error) {
console.error('Error submitting moderation:', error)
addNotification({
@@ -1900,7 +1929,7 @@ async function endChecklist(status?: string) {
if (!(await navigateToNextUnlockedProject())) {
// Fallback: batch check remaining projects with metadata
const remainingIds: string[] = []
const currentProjectId = projectV2.value.id
const currentProjectId = projectV2.value?.id
const queueItems = moderationStore.currentQueue.items
// Build list of remaining projects, excluding current
@@ -1971,10 +2000,21 @@ async function endChecklist(status?: string) {
}
async function skipCurrentProject() {
// Release the lock before skipping
await moderationStore.releaseLock(projectV2.value.id)
const projectId = projectV2.value?.id
if (!projectId) {
addNotification({
title: 'Error skipping project',
text: 'Project data unavailable. Please try again.',
type: 'error',
})
return
}
hasNextProject.value = moderationStore.completeCurrentProject(projectV2.value.id, 'skipped')
moderationStore.releaseLock(projectId).catch((err) => {
console.warn('Failed to release lock:', err)
})
hasNextProject.value = moderationStore.completeCurrentProject(projectId, 'skipped')
await endChecklist('skipped')
}
@@ -1,72 +0,0 @@
<template>
<div class="categories">
<slot />
<span
v-for="category in categoriesFiltered"
:key="category.name"
v-html="category.icon + formatCategory(category.name)"
/>
</div>
</template>
<script>
import { formatCategory } from '@modrinth/utils'
export default {
props: {
categories: {
type: Array,
default() {
return []
},
},
type: {
type: String,
required: true,
},
},
setup() {
const tags = useGeneratedState()
return { tags }
},
computed: {
categoriesFiltered() {
return this.tags.categories
.concat(this.tags.loaders)
.filter(
(x) =>
this.categories.includes(x.name) && (!x.project_type || x.project_type === this.type),
)
},
},
methods: { formatCategory },
}
</script>
<style lang="scss" scoped>
.categories {
display: flex;
flex-direction: row;
flex-wrap: wrap;
:deep(span) {
display: flex;
align-items: center;
flex-direction: row;
&:not(:last-child) {
margin-right: var(--spacing-card-md);
}
&:not(.badge) {
color: var(--color-icon);
}
svg {
width: 1rem;
margin-right: 0.2rem;
}
}
}
</style>
@@ -88,7 +88,7 @@
:open-by-default="!versionFilter"
:class="[
versionFilter ? '' : '!border-solid border-orange bg-bg-orange !text-contrast',
'flex flex-col gap-2 rounded-2xl border-2 border-dashed border-divider p-3 transition-all',
'flex flex-col gap-2 rounded-2xl border-2 border-dashed border-surface-5 p-3 transition-all',
]"
>
<p class="m-0 items-center font-bold">
@@ -132,17 +132,23 @@
:loader="'Vanilla'"
class="size-5 flex-none"
/>
<svg
v-else
<component
:is="getLoaderIcon(filtersRef.selectedPlatforms[0])"
v-else-if="
filtersRef?.selectedPlatforms[0] && getLoaderIcon(filtersRef.selectedPlatforms[0])
"
class="size-5 flex-none"
v-html="tags.loaders.find((x) => x.name === filtersRef?.selectedPlatforms[0])?.icon"
></svg>
/>
<div class="w-full truncate text-left">
{{
filtersRef?.selectedPlatforms.length === 0
? 'All platforms'
: filtersRef?.selectedPlatforms.map((x) => formatCategory(x)).join(', ')
: filtersRef?.selectedPlatforms
.map((x) => {
return formatLoader(formatMessage, x)
})
.join(', ')
}}
</div>
</template>
@@ -242,6 +248,7 @@ import {
DropdownIcon,
ExternalIcon,
GameIcon,
getLoaderIcon,
LockOpenIcon,
XIcon,
} from '@modrinth/assets'
@@ -252,10 +259,12 @@ import {
Checkbox,
Combobox,
CopyCode,
formatLoader,
NewModal,
TagItem,
useVIntl,
} from '@modrinth/ui'
import TagItem from '@modrinth/ui/src/components/base/TagItem.vue'
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from '@modrinth/utils'
import { formatVersionsForDisplay, type Mod, type Version } from '@modrinth/utils'
import { computed, ref } from 'vue'
import Accordion from '~/components/ui/Accordion.vue'
@@ -265,6 +274,8 @@ import ContentVersionFilter, {
} from '~/components/ui/servers/ContentVersionFilter.vue'
import LoaderIcon from '~/components/ui/servers/icons/LoaderIcon.vue'
const { formatMessage } = useVIntl()
const props = defineProps<{
type: 'Mod' | 'Plugin'
loader: string
@@ -424,7 +435,7 @@ const formattedVersions = computed(() => {
if (secondLoaderPosition === -1) return -1
return firstLoaderPosition - secondLoaderPosition
})
.map((loader: string) => formatCategory(loader)),
.map((loader: string) => formatLoader(formatMessage, loader)),
}
})
@@ -19,7 +19,7 @@
Platform
</slot>
<template #option="{ option }">
{{ formatCategory(option) }}
{{ formatLoader(formatMessage, String(option)) }}
</template>
<template v-if="hasAnyUnsupportedPlatforms" #footer>
<Checkbox
@@ -58,12 +58,15 @@
<script setup lang="ts">
import { FilterIcon } from '@modrinth/assets'
import { formatLoader, useVIntl } from '@modrinth/ui'
import Checkbox from '@modrinth/ui/src/components/base/Checkbox.vue'
import ManySelect from '@modrinth/ui/src/components/base/ManySelect.vue'
import { formatCategory, type GameVersionTag, type Version } from '@modrinth/utils'
import type { GameVersionTag, Version } from '@modrinth/utils'
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
const { formatMessage } = useVIntl()
export type ListedGameVersion = {
name: string
release: boolean
@@ -119,7 +122,7 @@ const hasOnlyUnsupportedPlatforms = computed(() => {
const showSupportedPlatformsOnly = ref(true)
const filterOptions = computed(() => {
const filterOptions = computed<Record<'gameVersion' | 'platform', string[]>>(() => {
const filters: Record<'gameVersion' | 'platform', string[]> = {
gameVersion: [],
platform: [],
@@ -32,12 +32,7 @@
<label class="w-full text-lg font-bold text-contrast" for="modpack-hard-reset">
Erase all data
</label>
<input
id="modpack-hard-reset"
v-model="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
/>
<Toggle id="modpack-hard-reset" v-model="hardReset" class="shrink-0" />
</div>
<div>
If enabled, existing mods, worlds, and configurations, will be deleted before installing
@@ -69,7 +64,7 @@
<script setup lang="ts">
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, Combobox, injectNotificationManager, NewModal } from '@modrinth/ui'
import { ButtonStyled, Combobox, injectNotificationManager, NewModal, Toggle } from '@modrinth/ui'
import { ModrinthServersFetchError } from '@modrinth/utils'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
@@ -158,9 +153,3 @@ const hide = () => modal.value?.hide()
defineExpose({ show, hide })
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>
@@ -53,12 +53,7 @@
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
Erase all data
</label>
<input
id="hard-reset"
v-model="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
/>
<Toggle id="hard-reset" v-model="hardReset" class="shrink-0" />
</div>
<div>
Removes all data on your server, including your worlds, mods, and configuration
@@ -128,6 +123,7 @@ import {
ButtonStyled,
injectNotificationManager,
NewModal,
Toggle,
} from '@modrinth/ui'
import { ModrinthServersFetchError } from '@modrinth/utils'
import { onMounted, onUnmounted } from 'vue'
@@ -255,9 +251,3 @@ const hide = () => mrpackModal.value?.hide()
defineExpose({ show, hide })
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>
@@ -143,12 +143,7 @@
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
Erase all data
</label>
<input
id="hard-reset"
v-model="hardReset"
class="switch stylized-toggle shrink-0"
type="checkbox"
/>
<Toggle id="hard-reset" v-model="hardReset" class="shrink-0" />
</div>
<div>
Removes all data on your server, including your worlds, mods, and configuration files,
@@ -542,9 +537,3 @@ const hide = () => versionSelectModal.value?.hide()
defineExpose({ show, hide })
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
</style>
@@ -77,11 +77,17 @@
</ButtonStyled>
</div>
<NewProjectCard
<ProjectCard
v-if="!versionsError && !currentVersionError"
class="!cursor-default !bg-bg !filter-none"
:project="projectCardData"
:categories="data.project?.categories || []"
class="!bg-bg"
:title="projectCardData.title"
:icon-url="projectCardData.icon_url"
:date-updated="projectCardData.date_modified"
:followers="projectCardData.follows"
:downloads="projectCardData.downloads"
layout="list"
:summary="projectCardData.description"
:tags="data.project?.categories || []"
>
<template #actions>
<ButtonStyled color="brand">
@@ -91,7 +97,7 @@
</button>
</ButtonStyled>
</template>
</NewProjectCard>
</ProjectCard>
</div>
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
<ButtonStyled>
@@ -159,7 +165,7 @@
<script setup lang="ts">
import { CompassIcon, InfoIcon, SettingsIcon, TransferIcon, UploadIcon } from '@modrinth/assets'
import { ButtonStyled, NewProjectCard, useVIntl } from '@modrinth/ui'
import { ButtonStyled, ProjectCard, useVIntl } from '@modrinth/ui'
import type { Loaders } from '@modrinth/utils'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
@@ -274,10 +280,6 @@ watch(
</script>
<style scoped>
.stylized-toggle:checked::after {
background: var(--color-accent-contrast) !important;
}
.button-base:active {
scale: none !important;
}
@@ -25,7 +25,7 @@
v-if="isOpen"
ref="menuRef"
data-pyro-telepopover-root
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-divider bg-bg-raised p-2 shadow-lg"
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-surface-5 bg-bg-raised p-2 shadow-lg"
:style="menuStyle"
role="menu"
tabindex="-1"
@@ -1,22 +1,15 @@
<template>
<div v-if="loaderData?.icon" v-html="loaderData.icon" />
<component :is="icon" v-if="icon" />
<LoaderIcon v-else />
</template>
<script setup lang="ts">
import { LoaderIcon } from '@modrinth/assets'
import { getLoaderIcon, LoaderIcon } from '@modrinth/assets'
import { computed } from 'vue'
import { useGeneratedState } from '~/composables/generated'
const props = defineProps<{
loader: string
}>()
const tags = useGeneratedState()
// Find the loader by name (case-insensitive comparison)
const loaderData = computed(() =>
tags.value.loaders.find((l) => l.name.toLowerCase() === props.loader.toLowerCase()),
)
const icon = computed(() => getLoaderIcon(props.loader))
</script>
@@ -10,12 +10,14 @@
<MedalIcon class="h-8 w-auto text-contrast md:h-10" />
<div class="flex flex-col items-start gap-1">
<span>
Try a free
<span class="text-medal-orange">3GB server</span> for 5 days powered by
<span class="text-medal-orange">Medal</span>
<IntlFormatted :message-id="messages.info">
<template #orange="{ children }">
<span class="text-medal-orange"><component :is="() => children" /></span>
</template>
</IntlFormatted>
</span>
<span class="text-xs font-medium text-secondary md:text-sm">
Limited-time offer. No credit card required. Available for US servers.
{{ formatMessage(messages.textSecondary) }}
</span>
</div>
</div>
@@ -23,7 +25,7 @@
<nuxt-link
to="https://medal.tv/modrinth"
class="z-10 flex w-full items-center justify-center gap-1 md:mt-0 md:w-auto"
>Learn more <ExternalIcon
>{{ formatMessage(messages.learnMoreButton) }} <ExternalIcon
/></nuxt-link>
</ButtonStyled>
</div>
@@ -31,9 +33,33 @@
<script lang="ts" setup>
import { ExternalIcon } from '@modrinth/assets'
import { ButtonStyled, MedalBackgroundImage } from '@modrinth/ui'
import {
ButtonStyled,
defineMessages,
IntlFormatted,
MedalBackgroundImage,
useVIntl,
} from '@modrinth/ui'
import MedalIcon from '~/assets/images/illustrations/medal_icon.svg?component'
const { formatMessage } = useVIntl()
const messages = defineMessages({
info: {
id: 'hosting-marketing.medal.info',
defaultMessage:
'Try a free <orange>3GB server</orange> for 5 days powered by <orange>Medal</orange>',
},
textSecondary: {
id: 'hosting-marketing.medal.text-secondary',
defaultMessage: 'Limited-time offer. No credit card required. Available for US servers.',
},
learnMoreButton: {
id: 'hosting-marketing.medal.learn-more',
defaultMessage: 'Learn more',
},
})
</script>
<style scoped lang="scss">
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { MessageDescriptor } from '@modrinth/ui'
import { ButtonStyled, defineMessage, ServersSpecs, useVIntl } from '@modrinth/ui'
import { ButtonStyled, defineMessage, defineMessages, ServersSpecs, useVIntl } from '@modrinth/ui'
import { formatPrice } from '@modrinth/utils'
const { formatMessage, locale } = useVIntl()
@@ -11,6 +11,17 @@ const emit = defineEmits<{
type Plan = 'small' | 'medium' | 'large'
const messages = defineMessages({
outOfStock: {
id: 'hosting.plan.out-of-stock',
defaultMessage: 'Out of stock',
},
selectPlanButton: {
id: 'hosting.plan.select-plan',
defaultMessage: 'Select plan',
},
})
const plans: Record<
Plan,
{
@@ -134,8 +145,12 @@ const billingMonths = computed(() => {
:type="plans[plan].mostPopular ? 'standard' : 'highlight-colored-text'"
size="large"
>
<span v-if="outOfStock" class="button-like disabled"> Out of Stock </span>
<button v-else @click="() => emit('select')">Select plan</button>
<span v-if="outOfStock" class="button-like disabled">{{
formatMessage(messages.outOfStock)
}}</span>
<button v-else @click="() => emit('select')">
{{ formatMessage(messages.selectPlanButton) }}
</button>
</ButtonStyled>
<ServersSpecs
:ram="ram"
@@ -323,6 +323,57 @@ export const scopeMessages = defineMessages({
},
})
export const scopeCategoryMessages = defineMessages({
categoryUserAccount: {
id: 'scopes.category.user-account',
defaultMessage: 'User account',
},
categoryProjects: {
id: 'scopes.category.projects',
defaultMessage: 'Projects',
},
categoryVersions: {
id: 'scopes.category.versions',
defaultMessage: 'Versions',
},
categoryCollections: {
id: 'scopes.category.collections',
defaultMessage: 'Collections',
},
categoryOrganizations: {
id: 'scopes.category.organizations',
defaultMessage: 'Organizations',
},
categoryReports: {
id: 'scopes.category.reports',
defaultMessage: 'Reports',
},
categoryThreads: {
id: 'scopes.category.threads',
defaultMessage: 'Threads',
},
categoryPats: {
id: 'scopes.category.pats',
defaultMessage: 'PATs',
},
categorySessions: {
id: 'scopes.category.sessions',
defaultMessage: 'Sessions',
},
categoryNotifications: {
id: 'scopes.category.notifications',
defaultMessage: 'Notifications',
},
categoryPayouts: {
id: 'scopes.category.payouts',
defaultMessage: 'Payouts',
},
categoryAnalytics: {
id: 'scopes.category.analytics',
defaultMessage: 'Analytics',
},
})
const scopeDefinitions = [
{
id: 'USER_READ_EMAIL',
@@ -29,8 +29,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
projectTypesPrimaryNav: false,
enableMedalPromotion: true,
hidePlusPromoInUserMenu: false,
oldProjectCards: true,
newProjectCards: false,
projectBackground: false,
searchBackground: false,
advancedDebugInfo: false,
@@ -43,6 +41,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
serverDiscovery: false,
disablePrettyProjectUrlRedirects: false,
hidePreviewBanner: false,
showDiscoverProjectButtons: false,
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS

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