Compare commits

..
Author SHA1 Message Date
Calum H. (IMB11) e2d5b3692a fix: pass game_versions not games 2026-06-25 20:41:32 +01:00
aecsocket 8cdd555cc5 Merge branch 'cal/move-install-to-app-backend' into tmp-staging-oauth-content 2026-06-25 20:00:22 +01:00
aecsocket 8ec9975307 Merge branch 'boris/dev-908-backend-changes' into tmp-staging-oauth-content 2026-06-25 20:00:17 +01:00
Calum H. (IMB11) 89602c195d fix: fmt 2026-06-25 19:58:41 +01:00
Calum H. (IMB11) 45ed092d49 fix: pass ro_pool 2026-06-25 19:49:50 +01:00
Calum H. (IMB11) 254c69cf4e fix: clippy 2026-06-25 19:25:35 +01:00
Calum H. (IMB11) 0f5fafa146 fix: prepr 2026-06-25 19:22:00 +01:00
Calum H. (IMB11) 21f256fef8 fix: tombi 2026-06-25 19:20:27 +01:00
Calum H. (IMB11) 32007aa559 fix: lint 2026-06-25 19:15:45 +01:00
Calum H. (IMB11) cebfa04395 fix: install state issue 2026-06-25 19:15:45 +01:00
Calum H. (IMB11) 75f40cdda8 feat: rev changes 2026-06-25 19:14:21 +01:00
tdgao 24285baebb fmt 2026-06-25 11:58:40 -06:00
Calum H. (IMB11) e2515369a3 fix: lint 2026-06-25 18:45:58 +01:00
Calum H. (IMB11) e74ec144c7 fix: fmt 2026-06-25 18:45:58 +01:00
Calum H. (IMB11) 4fea22819d fix: add redis caching 2026-06-25 18:45:58 +01:00
Calum H. (IMB11) b925f2dbec feat: shared modrinth-content-management crate 2026-06-25 18:45:58 +01:00
Calum H. (IMB11) 46b17b3175 fix: lint 2026-06-25 18:15:15 +01:00
Calum H. (IMB11) db8c999268 fix: lint 2026-06-25 18:14:23 +01:00
Calum H. (IMB11) 258957db3c feat: optimise queries + adapter fns 2026-06-25 18:14:03 +01:00
Calum H. (IMB11) ab53a49bbc fix: worlds.vue improvements + browse page fixes 2026-06-25 18:14:03 +01:00
Calum H. (IMB11) 0caa447228 fix: navtabs anim not working in app 2026-06-25 18:14:03 +01:00
Calum H. (IMB11) f27847d34a fix: prepr 2026-06-25 18:14:03 +01:00
Calum H. (IMB11) b49132a9a6 fix: lint 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 0afbcf785c fix: install progress spacing + change mc/content/overrides to bytes 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) a430614c9f fix: fmt 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) f3104c7d1f fix: shear 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 91ae871bc7 fix: bug of hell 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 7e8bdcaf4a feat: defuck the installing process up 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 67d45f5039 fix: bugs 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 3e7e4ece1b fix: lint 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 2e6d5d743c fix: sqlx prepare + use sqlx macros 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 35b7f05167 fix: lint 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) a89634687f feat: app db backup after update 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 0144943ef4 fix: change to jsonb 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) c96c97e9d9 fix: rem transitions 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) b816b30a0b fix: feedback on bulk update + lint 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) c0efdd8143 fix: remove preview_update_all 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) dc7c1cbe5d feat: better update all handling 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) fa57e0d148 fix: updater modal cleanup w/new structure 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) ed29013a56 fix: check_content_updates mismatch 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 32d3cdc5b3 feat: break up app-lib/api/instance.rs 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) e4ce0bda8c fix: content missing fields 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) d6fb569875 fix: cargo fmt 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 4250fa6fb1 fix: add update proj multiselect guard 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 6db02be4ff fix: frontend still using profile path 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 4e84215bd3 fix: skins migration fix 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 3236acfee3 fix: install_from using profile 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) a5e73aa4fc prototype: instances v2 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) ceee3a064c feat: use old profiles with compat layer 2026-06-25 18:13:30 +01:00
Calum H. (IMB11) 81cb36548d feat: base of instances v2 2026-06-25 18:13:29 +01:00
Truman GaoandGitHub ae939cd4ca Merge branch 'main' into boris/dev-908-backend-changes 2026-06-25 11:13:28 -06:00
DeDiamondProandGitHub 8faa36b014 feat: add passkey support (#6375)
* feat: add passkey backend

* feat: passkey frontend

* invalidate sessions on compromised passkey

* chore: run sqlx prepare

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

* fix: correctly verify max passkeys in finish route

* fix: use structs for response

* fix: add rp name default

* style: use web::Json
2026-06-25 10:13:08 -07:00
tdgao a45a320512 fix: add stroke 2026-06-24 12:48:10 -06:00
tdgao 98c70633ee Merge branch 'main' into boris/dev-908-backend-changes 2026-06-24 11:05:06 -06:00
tdgao d858901568 Revert "Merge branch 'main' into boris/dev-908-backend-changes"
This reverts commit b9b03796e3.
2026-06-24 10:58:08 -06:00
tdgao b9b03796e3 Merge branch 'main' into boris/dev-908-backend-changes 2026-06-24 10:49:12 -06:00
tdgao e8475086ec fix: unused import 2026-06-23 13:19:40 -06:00
tdgao 655f899785 Merge branch 'main' into boris/dev-908-backend-changes 2026-06-23 12:48:00 -06:00
tdgao 9bcee5ab52 feat: update sign in screen 2026-06-23 12:45:18 -06:00
tdgao 9f050f21e6 improve reset password 2026-06-23 12:03:59 -06:00
tdgao 782c82d3e8 update copy 2026-06-23 11:54:38 -06:00
tdgao da828f4db8 update copy 2026-06-23 11:46:50 -06:00
aecsocket f6f2cb8d8b Merge branch 'main' into boris/dev-908-backend-changes 2026-06-22 19:50:47 +01:00
aecsocket 1a0633b6db Merge branch 'main' into boris/dev-908-backend-changes 2026-06-11 21:31:23 +01:00
aecsocket 3ae50a2abb Merge branch 'main' into boris/dev-908-backend-changes 2026-06-11 20:09:58 +01:00
tdgao 408927a23e Merge branch 'main' into boris/dev-908-backend-changes 2026-06-10 11:58:27 -07:00
tdgao 3c29a62036 fix: 2 factor auth enter code screen styles 2026-05-20 12:14:35 -06:00
aecsocket 407aa4b4cf Merge branch 'main' into boris/dev-908-backend-changes 2026-05-13 12:22:56 +01:00
aecsocket d709bbeb54 squash commits 2026-05-12 15:44:12 +01:00
Prospectorandaecsocket b5662371d8 pin tanstack versions + set pnpm min age to 7 days 2026-05-12 12:28:55 +01:00
Prospectorandaecsocket db9689b335 fix: open modrinth project links in the app (#6072) 2026-05-12 12:28:55 +01:00
Truman Gaoandaecsocket 0f1483d9df feat: improve add dependency flow (#6075)
* fix: shadow on nav

* feat: improve add dependency flow

* feat: update suggested dependency style

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

* feat: implement combobox select searched text on focus

* feat: add Tabs.vue

* feat: update nav tabs to use tabs

* feat: improve project search dropdown

* fix: dependency search not clearing inbound query

* fix: combobox no options open state bug

* feat: improve dependency project and version search
2026-05-12 12:28:55 +01:00
Truman Gaoandaecsocket d48ad873fb fix: cannot hover over project card tooltip items (#6071)
fix: cannot hover over project cards
2026-05-12 12:28:55 +01:00
tdgao ff7c0cb5b8 fix: edit copy on info banner 2026-05-11 15:13:06 -06:00
tdgao faed8c3f6d fix: remove default date to open 13 years ago 2026-05-11 15:02:46 -06:00
tdgao 63d00cb660 fix: account.vue 2026-05-11 14:55:26 -06:00
tdgao 27d54a98af Merge branch 'main' into boris/dev-908-backend-changes 2026-05-11 14:50:39 -06:00
aecsocket 05a1a10788 fmt 2026-04-30 21:48:37 +01:00
tdgao addb6c6045 feat: add default date open view 2026-04-30 14:05:37 -06:00
aecsocket e08da23fb5 fix lints 2026-04-30 20:39:00 +01:00
tdgao ecd69f88b2 fix: hover color bad on number input 2026-04-30 11:19:56 -06:00
tdgao d834ce3645 fix: add width on date 2026-04-30 11:00:08 -06:00
tdgao cf58559381 feat: improve styles 2026-04-30 10:55:05 -06:00
tdgao 01a7ee8d1e fix: date picker icons 2026-04-30 10:28:23 -06:00
tdgao 5480826a53 fix: number input icons color 2026-04-30 10:19:01 -06:00
tdgao 9184de92df fix: calculate age based on user's timezone 2026-04-30 10:11:58 -06:00
tdgao d753b8735c fix: date picker can be null 2026-04-30 10:07:48 -06:00
aecsocket c7a5336a51 increase recursion limit 2026-04-28 23:56:47 +01:00
aecsocket 292b8f4cd2 Merge branch 'main' into boris/dev-908-backend-changes 2026-04-28 23:38:50 +01:00
aecsocket c391fb12b9 update rust toolchain 2026-04-28 23:36:43 +01:00
tdgao 62f20b9aee feat: implement preserve date for date picker 2026-04-27 14:57:20 -06:00
tdgao 2aca44bbcc feat: implement date picker component in create account 2026-04-27 14:16:32 -06:00
tdgao b9be6abf0b fix: range select border styles 2026-04-27 14:06:30 -06:00
tdgao a5be0114c5 refactor: pnpm prepr 2026-04-27 13:49:09 -06:00
tdgao f32f732c31 feat: improve UX and styles for range select 2026-04-27 13:35:32 -06:00
tdgao e974672e71 feat: implement date picker component 2026-04-27 13:18:27 -06:00
tdgao d6356ea8e5 feat: small style updates 2026-04-27 12:19:47 -06:00
tdgao 872dcbcf78 fix: authorize.vue error states 2026-04-27 12:03:43 -06:00
tdgao 44da0ed5ff refactor: convert authorize.vue to use typescript 2026-04-27 11:49:17 -06:00
tdgao ec7c893681 refactor: convert verify-email to use typescript 2026-04-27 11:14:58 -06:00
tdgao 3ce8f85463 refactor: use typescript for reset-password 2026-04-27 11:13:12 -06:00
Calum H.andGitHub 4ce82b01e6 Merge branch 'main' into boris/dev-908-backend-changes 2026-04-27 13:12:48 +01:00
tdgao f4d65437df refactor: pnpm prepr 2026-04-24 15:58:57 -06:00
tdgao 9646083025 refactor: use typescript 2026-04-24 15:55:52 -06:00
tdgao 652a9b0b5c fix: add initOnMounted for SSR race 2026-04-24 15:37:32 -06:00
tdgao 918c4a9c83 fix: last signed in style 2026-04-24 15:19:05 -06:00
tdgao 0a0b19233d Merge branch 'boris/dev-908-backend-changes' of github.com:modrinth/code into boris/dev-908-backend-changes 2026-04-24 15:10:43 -06:00
tdgao 6f2e04c3de feat: handle app create account 2026-04-24 15:10:41 -06:00
Calum H. (IMB11) ece35f9158 Revert "chore: temp staging undo"
This reverts commit cad6bd4f92.
2026-04-24 22:04:42 +01:00
Calum H. (IMB11) 3ccb8132d8 Merge branch 'boris/dev-908-backend-changes' of https://github.com/modrinth/code into boris/dev-908-backend-changes 2026-04-24 22:02:50 +01:00
Calum H. (IMB11) cad6bd4f92 chore: temp staging undo 2026-04-24 22:02:49 +01:00
tdgao 65bd8937a6 fix: hover effect on native date picker 2026-04-24 14:55:06 -06:00
tdgao abcac5f40a refactor: pnpm prepr 2026-04-24 14:46:05 -06:00
tdgao 43893158ac fix: qa issues 2026-04-24 14:43:57 -06:00
tdgao fcab720943 fix: last sign in badge color 2026-04-24 13:36:31 -06:00
tdgao 4183702a72 refactor: pnpm prepr 2026-04-24 13:02:46 -06:00
tdgao 8946e4c394 fix: append number when generated name from email is taken 2026-04-24 13:00:51 -06:00
tdgao f1ce31001a feat: implement last signed in indicator 2026-04-24 12:51:05 -06:00
tdgao c99564f8ea refactor: pnpm prepr 2026-04-24 11:38:39 -06:00
tdgao 71c1deb5cd fix: account settings modals to use new modal and fix lots of bad styles 2026-04-24 11:37:41 -06:00
tdgao 042dde075b refactor: bring root page card styles into individual components and use tailwind 2026-04-24 11:12:49 -06:00
tdgao 3bfe670d47 remove: auth/welcome page as its no longer used 2026-04-24 10:19:56 -06:00
tdgao ed53fe5330 fix: copy 2026-04-24 10:19:38 -06:00
tdgao 547921badc fix: hcaptcha styles 2026-04-24 10:12:05 -06:00
Michael H. aac61dcfaa fix: make sure staging uses staging 2026-04-24 16:26:21 +02:00
tdgao 52a972d949 refactor: pnpm prepr 2026-04-22 12:18:42 -06:00
tdgao bdc2b4abf8 Merge branch 'main' into boris/dev-908-backend-changes 2026-04-22 09:00:20 -06:00
Truman GaoandGitHub c19ce11ea4 feat: new auth flow (#5840)
* update auth with new designs

* refactor: auth.js to auth.ts

* refactor: componentize auth pages

* fix: auth pages height

* feat: initial implementation of new sign-in oauth

* fix create account flow

* fix checkbox

* remove hard coded username

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

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

* fix: TOCTOU issue
2026-04-22 07:57:17 -07:00
tdgao c54906e6de Merge branch 'main' into boris/dev-908-backend-changes 2026-04-17 17:08:04 -06:00
tdgao 57b4f21080 fix: backend response for create oauth account 2026-04-17 09:45:32 -06:00
aecsocket 66f3c39c13 fix flow 2026-04-14 19:24:30 +01:00
aecsocket 7484afa18e Revert "Use user-provided callback addr instead of SELF_ADDR"
This reverts commit 7ea0635d86.
2026-04-14 19:16:14 +01:00
aecsocket 7ea0635d86 Use user-provided callback addr instead of SELF_ADDR 2026-04-14 18:20:14 +01:00
aecsocket fe3aba52ab improve URL-related OAuth code 2026-04-14 12:35:13 +01:00
aecsocket 64f87551ff fix up oauth flow routes 2026-04-13 14:19:22 +01:00
aecsocket c8a586c6f1 Backend routes for choosing username in OAuth flow 2026-04-12 21:38:04 +01:00
68 changed files with 399 additions and 1174 deletions
+22
View File
@@ -73,6 +73,17 @@ jobs:
docker:
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
env:
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
needs: [skip-if-clean]
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
steps:
@@ -94,6 +105,17 @@ jobs:
~/.cargo/bin
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Mount sccache disk cache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
with:
key: ${{ github.repository }}-daedalus-sccache
path: /mnt/sccache
- name: Setup sccache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: Build daedalus_client
run: cargo build --release --package daedalus_client
+24
View File
@@ -76,6 +76,16 @@ jobs:
env:
SQLX_OFFLINE: 'true'
GIT_HASH: ${{ github.sha }}
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -86,6 +96,9 @@ jobs:
rustflags: ''
cache: false
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Install build dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
@@ -101,6 +114,17 @@ jobs:
~/.cargo/bin
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Mount sccache disk cache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
with:
key: ${{ github.repository }}-labrinth-sccache
path: /mnt/sccache
- name: Setup sccache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: Build labrinth
run: cargo build --profile release-labrinth --package labrinth
+13
View File
@@ -44,6 +44,16 @@ jobs:
name: Build
env:
VITE_STRIPE_PUBLISHABLE_KEY: pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5
# SCCACHE_DIR: '/mnt/sccache'
# SCCACHE_CACHE_SIZE: '10G'
# SCCACHE_MULTILEVEL_CHAIN: 'disk,s3'
SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/'
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
RUSTC_WRAPPER: 'sccache'
strategy:
fail-fast: false
matrix:
@@ -74,6 +84,9 @@ jobs:
rustflags: ''
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: Enable Corepack
run: corepack enable
+39 -1
View File
@@ -18,11 +18,25 @@ jobs:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check.outputs.skip }}
internal: ${{ steps.check-internal.outputs.internal }}
if: ${{ always() }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check if workflow runs on an internal branch
id: check-internal
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
REPO: ${{ github.repository }}
run: |
if [ "$EVENT_NAME" != "pull_request" ] || [ "$HEAD_REPO" = "$REPO" ]; then
echo "internal=true" >> $GITHUB_OUTPUT
else
echo "internal=false" >> $GITHUB_OUTPUT
fi
- name: Merge Queue CI Check Skipper
id: merge-queue-ci-skipper
uses: ./.github/merge-queue-ci-skipper
@@ -48,7 +62,7 @@ jobs:
build:
name: Lint and Test
runs-on: ubuntu-latest
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
needs: [skip-if-clean]
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
@@ -62,6 +76,19 @@ jobs:
# since we don't want warnings to become errors
# while developing)
RUSTFLAGS: -Dwarnings
# sccache config (only populated for internal branches; secrets aren't
# available to forked PRs, and blacksmith stickydisk requires a
# blacksmith runner)
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
steps:
- name: Check out code
@@ -111,6 +138,17 @@ jobs:
~/.cargo/bin
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Mount sccache disk cache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
with:
key: ${{ github.repository }}-turbo-sccache
path: /mnt/sccache
- name: Setup sccache
if: needs.skip-if-clean.outputs.internal == 'true'
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: Setup binstall
uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
Generated
-1
View File
@@ -10807,7 +10807,6 @@ dependencies = [
"tracing-error",
"tracing-subscriber",
"url",
"urlencoding",
"uuid 1.23.3",
"whoami",
"windows",
+7 -15
View File
@@ -95,7 +95,7 @@ import { check_reachable } from '@/helpers/auth.js'
import { get_user, get_version } from '@/helpers/cache.js'
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
import { list, run } from '@/helpers/instance'
import { list } from '@/helpers/instance'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
@@ -110,7 +110,6 @@ import {
isNetworkMetered,
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
import i18n from '@/i18n.config'
import {
appUpdateState,
@@ -869,14 +868,6 @@ async function handleCommand(e) {
source: 'CreationModalFileDrop',
})
}
} else if (e.event === 'LaunchInstance') {
if (e.server) {
await start_join_server(e.id, e.server).catch(handleError)
} else if (e.singleplayer_world) {
await start_join_singleplayer_world(e.id, e.singleplayer_world).catch(handleError)
} else {
await run(e.id).catch(handleError)
}
} else if (e.event === 'InstallServer') {
await router.push(`/project/${e.id}`)
await playServerProject(e.id).catch(handleError)
@@ -1341,11 +1332,12 @@ async function processPendingSurveys() {
const creds = await getCreds().catch(handleError)
const userId = creds?.user_id
const instances = (await list().catch(handleError)) ?? []
const isActivePlayer = instances.some(
(instance) =>
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
)
const instances = await list().catch(handleError)
const isActivePlayer =
instances.findIndex(
(instance) =>
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
) >= 0
let surveys = []
try {
@@ -35,7 +35,6 @@ import {
import { get_loader_versions } from '@/helpers/metadata'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { injectInstanceSettings } from '@/providers/instance-settings'
import { useTheming } from '@/store/state'
import type { Manifest } from '../../../helpers/types'
@@ -44,12 +43,8 @@ const filePicker = injectFilePicker()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debug = useDebugLogger('AppInstallationSettings')
const themeStore = useTheming()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const skipNonEssentialWarnings = computed(() =>
themeStore.getFeatureFlag('skip_non_essential_warnings'),
)
debug('metadata load: start', {
instanceId: instance.value.id,
@@ -216,7 +211,6 @@ provideInstallationSettings({
reinstalling.value ||
!!offline,
),
skipNonEssentialWarnings,
modpack: computed(() => {
if (isImportedModpack.value && instance.value.link?.type === 'imported_modpack') {
return {
@@ -11,7 +11,6 @@ const themeStore = useTheming()
const { formatMessage } = useVIntl()
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
@@ -102,15 +101,6 @@ const messages = defineMessages({
defaultMessage:
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
},
skipNonEssentialWarningsTitle: {
id: 'app.appearance-settings.skip-non-essential-warnings.title',
defaultMessage: 'Skip non-essential warnings',
},
skipNonEssentialWarningsDescription: {
id: 'app.appearance-settings.skip-non-essential-warnings.description',
defaultMessage:
'Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown.',
},
showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time',
@@ -295,25 +285,6 @@ watch(
/>
</div>
<div class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
@@ -8,12 +8,9 @@ import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { purge_cache_types } from '@/helpers/cache.js'
import { get, set } from '@/helpers/settings.ts'
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const themeStore = useTheming()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
watch(
settings,
@@ -52,15 +49,6 @@ async function purgeCache() {
]).catch(handleError)
}
function handlePurgeCacheClick() {
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
void purgeCache()
return
}
purgeCacheConfirmModal.value?.show()
}
async function openDbBackupsFolder() {
await showAppDbBackupsFolder().catch(handleError)
}
@@ -114,7 +102,7 @@ async function findLauncherDir() {
@proceed="purgeCache"
/>
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
<TrashIcon />
Purge cache
</button>
@@ -2,7 +2,6 @@
import {
ClipboardCopyIcon,
EditIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
IssuesIcon,
@@ -24,7 +23,6 @@ import {
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
OverflowMenu,
SmartClickable,
TagItem,
@@ -41,7 +39,7 @@ import type { Component } from 'vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { copyToClipboard, createInstanceShortcut } from '@/helpers/utils'
import { copyToClipboard } from '@/helpers/utils'
import type {
ProtocolVersion,
ServerStatus,
@@ -62,7 +60,6 @@ const formatDateTime = useFormatDateTime({
})
const router = useRouter()
const { addNotification } = injectNotificationManager()
const emit = defineEmits<{
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
@@ -97,7 +94,6 @@ const props = withDefaults(
instanceId?: string
instanceName?: string
instanceIcon?: string
shortcutInstanceId?: string
}>(),
{
playingInstance: false,
@@ -117,7 +113,6 @@ const props = withDefaults(
instanceId: undefined,
instanceName: undefined,
instanceIcon: undefined,
shortcutInstanceId: undefined,
},
)
@@ -136,33 +131,6 @@ const serverIncompatible = computed(
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
const managed = computed(() => props.managed)
const shortcutInstanceId = computed(() => props.shortcutInstanceId ?? props.instanceId)
async function createShortcut() {
if (!shortcutInstanceId.value) return
try {
const shortcutPath = await createInstanceShortcut(
props.world.name,
shortcutInstanceId.value,
props.world.type === 'server'
? { server: (props.world as ServerWorld).address }
: { singleplayerWorld: (props.world as SingleplayerWorld).path },
)
if (!shortcutPath) return
addNotification({
type: 'success',
title: 'Shortcut created',
})
} catch (error) {
addNotification({
type: 'error',
title: 'Failed to create shortcut',
text: error instanceof Error ? error.message : '',
})
}
}
const messages = defineMessages({
hardcore: {
@@ -217,10 +185,6 @@ const messages = defineMessages({
id: 'instance.worlds.dont_show_on_home',
defaultMessage: `Don't show on Home`,
},
createShortcut: {
id: 'instance.worlds.create_shortcut',
defaultMessage: 'Create shortcut',
},
linkedServer: {
id: 'instance.worlds.linked_server',
defaultMessage: 'Managed by server project',
@@ -509,11 +473,6 @@ const messages = defineMessages({
})
},
},
{
id: 'create-shortcut',
shown: !!shortcutInstanceId,
action: () => createShortcut(),
},
{
divider: true,
shown: !instanceId,
@@ -558,10 +517,6 @@ const messages = defineMessages({
<UpdatedIcon aria-hidden="true" />
{{ formatMessage(commonMessages.refreshButton) }}
</template>
<template #create-shortcut>
<ExternalIcon aria-hidden="true" />
{{ formatMessage(messages.createShortcut) }}
</template>
<template #dont-show-on-home>
<XIcon aria-hidden="true" />
{{ formatMessage(messages.dontShowOnHome) }}
-1
View File
@@ -97,7 +97,6 @@ type ReleaseChannel = 'release' | 'beta' | 'alpha'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = {
enabled: boolean
metadata?: {
project_id: string
version_id: string
-17
View File
@@ -1,5 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { save } from '@tauri-apps/plugin-dialog'
import { get_full_path, get_mod_full_path } from '@/helpers/instance'
@@ -48,22 +47,6 @@ export async function showLauncherLogsFolder() {
return await invoke('plugin:utils|show_launcher_logs_folder', {})
}
export async function createInstanceShortcut(instanceName, instanceId, options = {}) {
const outputPath = await save({
defaultPath: `Modrinth - ${instanceName}`,
})
if (!outputPath) return null
return await invoke('plugin:shortcuts|create_instance_shortcut', {
instanceName,
instanceId,
outputPath,
server: options.server,
singleplayerWorld: options.singleplayerWorld,
})
}
export async function showAppDbBackupsFolder() {
return await invoke('plugin:utils|show_app_db_backups_folder', {})
}
@@ -128,12 +128,6 @@
"app.appearance-settings.show-play-time.title": {
"message": "Show play time"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Skip non-essential warnings"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Enables the ability to toggle the sidebar."
},
@@ -959,9 +953,6 @@
"instance.worlds.copy_address": {
"message": "Copy address"
},
"instance.worlds.create_shortcut": {
"message": "Create shortcut"
},
"instance.worlds.dont_show_on_home": {
"message": "Don't show on Home"
},
-3
View File
@@ -56,7 +56,6 @@ import {
provideServerInstallContent,
} from '@/providers/setup/server-install-content'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
@@ -68,7 +67,6 @@ const debugLog = useDebugLogger('Browse')
const router = useRouter()
const route = useRoute()
const themeStore = useTheming()
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
provideServerInstallContent(serverInstallContent)
@@ -579,7 +577,6 @@ const installContext = computed(() => {
queuedCount: queuedServerInstallCount.value,
selectedProjects: selectedServerInstallProjects.value,
isInstallingSelected: isInstallingQueuedServerInstalls.value,
skipNonEssentialWarnings: themeStore.getFeatureFlag('skip_non_essential_warnings'),
installProgress: queuedInstallProgress.value,
clearQueued: clearQueuedServerInstalls,
clearSelected: clearQueuedServerInstalls,
+2 -26
View File
@@ -197,10 +197,6 @@
id: 'export-mrpack',
action: () => exportModal?.show(),
},
{
id: 'create-shortcut',
action: () => createShortcut(),
},
]"
>
<MoreVerticalIcon />
@@ -208,7 +204,6 @@
<template #host-a-server> <ServerIcon /> Create a server </template>
<template #open-folder> <FolderOpenIcon /> Open folder </template>
<template #export-mrpack> <PackageIcon /> Export modpack </template>
<template #create-shortcut> <ExternalIcon /> Create shortcut </template>
</OverflowMenu>
</ButtonStyled>
</div>
@@ -326,7 +321,7 @@ import { get, get_full_path, kill, run } from '@/helpers/instance'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
import { showInstanceInFolder } from '@/helpers/utils.js'
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
import { injectServerInstall } from '@/providers/server-install'
import { handleSevereError } from '@/store/error.js'
@@ -335,7 +330,7 @@ import { useBreadcrumbs, useTheming } from '@/store/state'
dayjs.extend(duration)
dayjs.extend(relativeTime)
const { addNotification, handleError } = injectNotificationManager()
const { handleError } = injectNotificationManager()
const { playServerProject } = injectServerInstall()
const queryClient = useQueryClient()
const route = useRoute()
@@ -603,25 +598,6 @@ const repairInstance = async () => {
}
}
const createShortcut = async () => {
if (!instance.value) return
try {
const shortcutPath = await createInstanceShortcut(instance.value.name, instance.value.id)
if (!shortcutPath) return
addNotification({
type: 'success',
title: 'Shortcut created',
})
} catch (error: unknown) {
addNotification({
type: 'error',
title: `Error creating shortcut`,
text: `${error}`,
})
}
}
const handleRightClick = (event: MouseEvent) => {
const baseOptions = [
{ name: 'add_content' },
@@ -124,7 +124,6 @@ import { type InstanceContentData, loadInstanceContentData } from '@/helpers/ins
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
import { highlightModInInstance } from '@/helpers/utils.js'
import { injectContentInstall } from '@/providers/content-install'
import { useTheming } from '@/store/state'
const messages = defineMessages({
shareTitle: {
@@ -174,10 +173,6 @@ const { installingItems, installRevisionByInstance, installFailureRevisionByInst
const router = useRouter()
const queryClient = useQueryClient()
const debug = useDebugLogger('Mods:ContentUpdate')
const themeStore = useTheming()
const skipNonEssentialWarnings = computed(() =>
themeStore.getFeatureFlag('skip_non_essential_warnings'),
)
const props = defineProps<{
instance: GameInstance
@@ -1064,7 +1059,7 @@ async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.
isModpackUpdateDowngrade.value ||
versionChangesGameVersion(selectedVersion, props.instance.game_version)
if (skipNonEssentialWarnings.value || !shouldShowWarning) {
if (!shouldShowWarning) {
await handleModpackUpdateConfirm()
return
}
@@ -1311,7 +1306,6 @@ provideContentManager({
isPackLocked,
isBusy: isInstanceBusy,
isBulkOperating,
skipNonEssentialWarnings,
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
toggleEnabled: toggleDisableDebounced,
bulkEnableItems: (items: ContentItem[]) =>
@@ -98,7 +98,6 @@
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
"
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
:shortcut-instance-id="instance.id"
@play="() => joinWorld(world)"
@stop="() => emit('stop')"
@refresh="() => refreshServer((world as ServerWorld).address)"
@@ -33,7 +33,6 @@ import {
} from '@/helpers/instance'
import { get_game_versions } from '@/helpers/tags'
import type { GameInstance, InstanceLoader } from '@/helpers/types'
import { useTheming } from '@/store/state'
interface ModalRef {
show: (initialVersionId?: string) => void
hide: () => void
@@ -188,7 +187,6 @@ export function createContentInstall(opts: {
handleError: (err: unknown) => void
}): ContentInstallContext {
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const instances = ref<ContentInstallInstance[]>([])
const compatibleLoaders = ref<string[]>([])
const gameVersions = ref<string[]>([])
@@ -812,7 +810,7 @@ export function createContentInstall(opts: {
const packs = await list()
const existingPack = packs.find((pack) => pack.link?.project_id === project.id)
if (existingPack && !themeStore.getFeatureFlag('skip_non_essential_warnings')) {
if (existingPack) {
pendingModpackInstall = { project, version, source, callback, createInstanceCallback }
modpackAlreadyInstalledModalRef?.show(existingPack.name, existingPack.id)
return
@@ -21,12 +21,10 @@ import {
import { list } from '@/helpers/instance'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata.js'
import type { InstanceLoader } from '@/helpers/types'
import { useTheming } from '@/store/state'
export function setupCreationModal(notificationManager: AbstractWebNotificationManager) {
const { handleError } = notificationManager
const router = useRouter()
const themeStore = useTheming()
const installationModal =
useTemplateRef<ComponentExposed<typeof CreationFlowModal>>('installationModal')
@@ -73,7 +71,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
const instances = await list().catch(handleError)
const existingInstance = instances?.find((i) => i.link?.project_id === projectId)
if (existingInstance && !themeStore.getFeatureFlag('skip_non_essential_warnings')) {
if (existingInstance) {
pendingModpackCreation.value = { projectId, versionId, name, iconUrl }
installationModal.value?.hide()
modpackAlreadyInstalledModal.value?.show(existingInstance.name, existingInstance.id)
-1
View File
@@ -10,7 +10,6 @@ export const DEFAULT_FEATURE_FLAGS = {
server_project_qa: false,
server_ram_as_bytes_always_on: false,
always_show_app_controls: false,
skip_non_essential_warnings: false,
skip_unknown_pack_warning: false,
pride_fundraiser: true,
i18n_debug: false,
+1 -1
View File
@@ -47,7 +47,7 @@ async fn main() -> theseus::Result<()> {
for world in worlds {
println!(
"World: {:?}/{:?} played at {:?}: {:#?}",
world.instance_id,
world.profile,
world.world.name,
world.world.last_played,
world.world.details
+11 -13
View File
@@ -22,6 +22,11 @@ path-util = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_with = { workspace = true }
tauri = { workspace = true, features = [
"devtools",
"macos-private-api",
"protocol-asset",
] }
tauri-plugin-deep-link = { workspace = true }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-fs = { workspace = true }
@@ -40,10 +45,6 @@ url = { workspace = true }
urlencoding = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4"] }
[dependencies.tauri]
workspace = true
features = ["devtools", "macos-private-api", "protocol-asset"]
[build-dependencies]
tauri-build = { workspace = true, features = ["codegen"] }
@@ -53,22 +54,19 @@ tauri-plugin-updater = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-graphics.workspace = true
objc2-app-kit = { workspace = true, features = ["NSWindow"] }
objc2-app-kit = { workspace = true, features = [
"NSWindow",
] }
[target.'cfg(windows)'.dependencies]
webview2-com.workspace = true
windows-core.workspace = true
[target.'cfg(windows)'.dependencies.windows]
workspace = true
features = [
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_System_Com",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
]
] }
windows-core.workspace = true
[features]
# by default Tauri runs in production mode
-8
View File
@@ -242,14 +242,6 @@ fn main() {
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"shortcuts",
InlinedPlugin::new()
.commands(&["create_instance_shortcut"])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"utils",
InlinedPlugin::new()
-1
View File
@@ -105,7 +105,6 @@
"cache:default",
"files:default",
"settings:default",
"shortcuts:default",
"tags:default",
"utils:default",
"ads:default",
-1
View File
@@ -13,7 +13,6 @@ pub mod minecraft_skins;
pub mod mr_auth;
pub mod process;
pub mod settings;
pub mod shortcuts;
pub mod tags;
pub mod utils;
-55
View File
@@ -1,55 +0,0 @@
use crate::api::Result;
use std::path::Path;
use url::Url;
pub(super) const SHORTCUT_EXTENSION: &str = "desktop";
pub(super) async fn create_shortcut(
profile_name: &str,
launch_url: &Url,
output_path: &Path,
) -> Result<()> {
let target_path = std::env::current_exe()?;
tokio::fs::write(
output_path,
format!(
"[Desktop Entry]\n\
Type=Application\n\
Name={}\n\
Exec={} {}\n\
Icon=ModrinthApp\n\
Terminal=false\n\
Categories=Game;\n",
escape_desktop_entry_value(&format!("Launch {profile_name}")),
quote_desktop_exec_arg(&target_path.to_string_lossy()),
quote_desktop_exec_arg(launch_url.as_str()),
),
)
.await?;
use std::os::unix::fs::PermissionsExt;
let mut permissions = tokio::fs::metadata(output_path).await?.permissions();
permissions.set_mode(0o755);
tokio::fs::set_permissions(output_path, permissions).await?;
Ok(())
}
fn escape_desktop_entry_value(input: &str) -> String {
input
.replace('\\', "\\\\")
.replace('\n', "\\n")
.replace('\r', "")
}
fn quote_desktop_exec_arg(input: &str) -> String {
format!(
"\"{}\"",
input
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$")
.replace('`', "\\`")
)
}
-90
View File
@@ -1,90 +0,0 @@
use crate::api::Result;
use std::{
hash::{DefaultHasher, Hash, Hasher},
path::Path,
};
use url::Url;
pub(super) const SHORTCUT_EXTENSION: &str = "app";
pub(super) async fn create_shortcut(
profile_name: &str,
launch_url: &Url,
output_path: &Path,
) -> Result<()> {
let contents_dir = output_path.join("Contents");
let macos_dir = contents_dir.join("MacOS");
let resources_dir = contents_dir.join("Resources");
tokio::fs::create_dir_all(&macos_dir).await?;
tokio::fs::create_dir_all(&resources_dir).await?;
let executable_path = macos_dir.join("launch");
tokio::fs::write(
&executable_path,
format!(
"#!/bin/sh\nexec /usr/bin/open {}\n",
shell_quote(launch_url.as_str()),
),
)
.await?;
tokio::fs::write(
resources_dir.join("icon.icns"),
include_bytes!("../../../icons/icon.icns"),
)
.await?;
tokio::fs::write(
contents_dir.join("Info.plist"),
format!(r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>launch</string>
<key>CFBundleIdentifier</key>
<string>{}</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>CFBundleName</key>
<string>{}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
</dict>
</plist>
"#,
macos_shortcut_identifier(launch_url.as_str()),
escape_xml(&format!("Launch {profile_name}")),
),
)
.await?;
use std::os::unix::fs::PermissionsExt;
let mut permissions =
tokio::fs::metadata(&executable_path).await?.permissions();
permissions.set_mode(0o755);
tokio::fs::set_permissions(&executable_path, permissions).await?;
Ok(())
}
fn macos_shortcut_identifier(launch_url: &str) -> String {
let mut hasher = DefaultHasher::new();
launch_url.hash(&mut hasher);
format!("com.modrinth.instance-shortcut.{:x}", hasher.finish())
}
fn shell_quote(input: &str) -> String {
format!("'{}'", input.replace('\'', "'\\''"))
}
fn escape_xml(input: &str) -> String {
input
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
-114
View File
@@ -1,114 +0,0 @@
use crate::api::Result;
use std::path::{Path, PathBuf};
use tauri::Runtime;
use url::Url;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "linux")]
use linux::{SHORTCUT_EXTENSION, create_shortcut};
#[cfg(target_os = "macos")]
use macos::{SHORTCUT_EXTENSION, create_shortcut};
#[cfg(target_os = "windows")]
use windows::{SHORTCUT_EXTENSION, create_shortcut};
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("shortcuts")
.invoke_handler(tauri::generate_handler![create_instance_shortcut])
.build()
}
#[tauri::command]
pub async fn create_instance_shortcut(
instance_name: String,
instance_id: String,
output_path: PathBuf,
server: Option<String>,
singleplayer_world: Option<String>,
) -> Result<PathBuf> {
if server.is_some() && singleplayer_world.is_some() {
return Err(std::io::Error::other(
"shortcut cannot launch both a server and a singleplayer world",
)
.into());
}
let launch_url =
instance_launch_url(instance_id, server, singleplayer_world);
let output_path = shortcut_path_with_extension(output_path);
let output_path_existed =
tokio::fs::try_exists(&output_path).await.unwrap_or(false);
if let Err(error) =
create_shortcut(&instance_name, &launch_url, &output_path).await
{
cleanup_shortcut_artifact(&output_path, output_path_existed).await;
return Err(error);
}
Ok(output_path)
}
fn instance_launch_url(
instance_id: String,
server: Option<String>,
singleplayer_world: Option<String>,
) -> Url {
let mut launch_url = Url::parse("modrinth://launch/instance")
.expect("static launch URL should parse");
launch_url
.path_segments_mut()
.expect("launch URL should support path segments")
.push(&instance_id);
if let Some(server) = server {
launch_url.query_pairs_mut().append_pair("server", &server);
} else if let Some(singleplayer_world) = singleplayer_world {
launch_url
.query_pairs_mut()
.append_pair("singleplayer_world", &singleplayer_world);
}
launch_url
}
fn shortcut_path_with_extension(mut path: PathBuf) -> PathBuf {
if path
.extension()
.is_none_or(|current_extension| current_extension != SHORTCUT_EXTENSION)
{
path.set_extension(SHORTCUT_EXTENSION);
}
path
}
async fn cleanup_shortcut_artifact(path: &Path, existed: bool) {
if existed {
return;
}
let result = match tokio::fs::metadata(path).await {
Ok(metadata) if metadata.is_dir() => {
tokio::fs::remove_dir_all(path).await
}
_ => tokio::fs::remove_file(path).await,
};
if let Err(error) = result
&& error.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!(
"failed to clean up shortcut artifact {}: {}",
path.display(),
error
);
}
}
-125
View File
@@ -1,125 +0,0 @@
use crate::api::Result;
use std::{
os::windows::ffi::OsStrExt,
path::{Path, PathBuf},
};
use url::Url;
use windows::{
Win32::{
System::Com::{
CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
COINIT_DISABLE_OLE1DDE, CoCreateInstance, CoInitializeEx,
CoUninitialize, IPersistFile,
},
UI::Shell::{IShellLinkW, ShellLink},
},
core::{Interface, PCWSTR},
};
pub(super) const SHORTCUT_EXTENSION: &str = "lnk";
pub(super) async fn create_shortcut(
_profile_name: &str,
launch_url: &Url,
output_path: &Path,
) -> Result<()> {
let target_path = std::env::current_exe()?;
let working_dir = target_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_default();
let output_path = output_path.to_path_buf();
let launch_url = launch_url.to_string();
tokio::task::spawn_blocking(move || {
create_windows_shortcut(
output_path,
target_path,
working_dir,
launch_url,
)
})
.await
.map_err(|error| {
std::io::Error::other(format!(
"failed to join shortcut creation task: {error}"
))
})??;
Ok(())
}
fn create_windows_shortcut(
output_path: PathBuf,
target_path: PathBuf,
working_dir: PathBuf,
launch_url: String,
) -> std::io::Result<()> {
let output_path = windows_wide_path(&output_path);
let target_path = windows_wide_path(&target_path);
let working_dir = windows_wide_path(&working_dir);
let launch_url = windows_wide_string(&launch_url);
// SAFETY:
// - COM is initialized for this blocking thread before any COM object is created.
// - `_com` is declared before the COM interface values, so it is dropped
// after them and calls `CoUninitialize` only once they are released.
// - Every PCWSTR points to a NUL-terminated UTF-16 buffer that lives until
// each call using it has returned.
unsafe {
let init_result = CoInitializeEx(
None,
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE,
);
windows_result(init_result.ok())?;
let _com = WindowsComGuard;
let shortcut: IShellLinkW = windows_result(CoCreateInstance(
&ShellLink,
None,
CLSCTX_INPROC_SERVER,
))?;
windows_result(shortcut.SetPath(windows_pcwstr(&target_path)))?;
windows_result(shortcut.SetArguments(windows_pcwstr(&launch_url)))?;
windows_result(
shortcut.SetWorkingDirectory(windows_pcwstr(&working_dir)),
)?;
windows_result(
shortcut.SetIconLocation(windows_pcwstr(&target_path), 0),
)?;
let persist_file: IPersistFile = windows_result(shortcut.cast())?;
windows_result(persist_file.Save(windows_pcwstr(&output_path), true))?;
}
Ok(())
}
fn windows_result<T>(result: windows::core::Result<T>) -> std::io::Result<T> {
result.map_err(std::io::Error::other)
}
struct WindowsComGuard;
impl Drop for WindowsComGuard {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
fn windows_wide_path(path: &Path) -> Vec<u16> {
path.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn windows_wide_string(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
fn windows_pcwstr(value: &[u16]) -> PCWSTR {
PCWSTR::from_raw(value.as_ptr())
}
-7
View File
@@ -140,18 +140,11 @@ pub async fn get_opening_command(
state: tauri::State<'_, crate::macos::deep_link::InitialPayload>,
) -> Result<Option<CommandPayload>> {
let payload = state.payload.lock().await;
let cmd_arg = std::env::args_os()
.nth(1)
.map(|path| path.to_string_lossy().to_string());
return if let Some(payload) = payload.as_ref() {
tracing::info!("opening command {payload}");
Ok(Some(handler::parse_command(payload).await?))
} else if let Some(cmd_arg) = cmd_arg {
tracing::info!("opening command {cmd_arg:?}");
Ok(Some(handler::parse_command(&cmd_arg).await?))
} else {
Ok(None)
};
-1
View File
@@ -241,7 +241,6 @@ fn main() {
.plugin(api::minecraft_skins::init())
.plugin(api::process::init())
.plugin(api::settings::init())
.plugin(api::shortcuts::init())
.plugin(api::tags::init())
.plugin(api::utils::init())
.plugin(api::cache::init())
@@ -27,7 +27,7 @@
<div
v-for="passkey in passkeys"
:key="passkey.id"
class="universal-card recessed flex items-center justify-between"
class="universal-card recessed passkey"
>
<div>
<div>
@@ -54,45 +54,43 @@
</span>
</div>
</div>
<div class="flex gap-2">
<ButtonStyled circular>
<div class="passkey-actions flex gap-2">
<ButtonStyled>
<button
v-tooltip="formatMessage(commonMessages.renameButton)"
@click="
() => {
passkeyToRename = { ...passkey }
renamePasskeyModal?.show()
$refs.renamePasskeyModal.show()
}
"
>
<EditIcon />
<EditIcon /> {{ formatMessage(commonMessages.renameButton) }}
</button>
</ButtonStyled>
<ButtonStyled circular>
<ButtonStyled>
<button
v-tooltip="formatMessage(commonMessages.removeButton)"
@click="
() => {
passkeyToRemove = passkey
removePasskeyModal?.show()
$refs.removePasskeyModal.show()
}
"
>
<TrashIcon />
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
</button>
</ButtonStyled>
</div>
</div>
</template>
<div class="input-group self-end">
<ButtonStyled>
<ButtonStyled color="brand">
<button @click="registerPasskey()">
<PlusIcon />
{{ formatMessage(messages.managePasskeyAddPasskey) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="managePasskeyModal?.hide()">
<button @click="$refs.managePasskeyModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.closeButton) }}
</button>
@@ -108,30 +106,32 @@
>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<div class="text-md font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<label for="passkey-name">
<div class="text-md mb-2 font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<div class="label__description mb-2">
{{ formatMessage(messages.passkeyNameDescription) }}
</div>
</label>
<StyledInput
id="passkey-name"
v-model="pendingPasskeyName"
v-model="pendingPasskey.name"
:maxlength="255"
type="text"
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
@keyup.enter="pendingPasskeyName && finishRegisterPasskey()"
@keyup.enter="pendingPasskey.name && finishRegisterPasskey()"
/>
<div class="label__description">
{{ formatMessage(messages.passkeyNameDescription) }}
</div>
</div>
<div class="flex justify-end gap-2.5">
<div class="input-group push-right">
<ButtonStyled>
<button @click="addPasskeyModal?.hide()">
<button @click="$refs.addPasskeyModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="!pendingPasskeyName" @click="finishRegisterPasskey()">
<button :disabled="!pendingPasskey.name" @click="finishRegisterPasskey()">
<PlusIcon />
{{ formatMessage(messages.managePasskeyAddPasskey) }}
</button>
@@ -145,32 +145,34 @@
width="500px"
:header="formatMessage(messages.renamePasskeyModalHeader)"
>
<div v-if="passkeyToRename" class="flex flex-col gap-6">
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<div class="text-md font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<label for="passkey-rename">
<div class="text-md mb-2 font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<div class="label__description mb-2">
{{ formatMessage(messages.passkeyNameDescription) }}
</div>
</label>
<StyledInput
id="passkey-rename"
v-model="passkeyToRenameName"
v-model="passkeyToRename.name"
:maxlength="255"
type="text"
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
@keyup.enter="passkeyToRenameName && renamePasskey()"
@keyup.enter="passkeyToRename.name && renamePasskey()"
/>
<div class="label__description">
{{ formatMessage(messages.passkeyNameDescription) }}
</div>
</div>
<div class="flex justify-end gap-2.5">
<div class="input-group push-right">
<ButtonStyled>
<button @click="renamePasskeyModal?.hide()">
<button @click="$refs.renamePasskeyModal.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="!passkeyToRenameName" @click="renamePasskey()">
<button :disabled="!passkeyToRename.name" @click="renamePasskey()">
<SaveIcon />
{{ formatMessage(commonMessages.saveButton) }}
</button>
@@ -221,7 +223,6 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { computed, ref, useTemplateRef } from 'vue'
import { createPasskeyCredential } from '~/helpers/passkey.ts'
@@ -261,7 +262,7 @@ const messages = defineMessages({
},
managePasskeyModalNoPasskeys: {
id: 'settings.account.security.passkey.modal.no-passkeys',
defaultMessage: 'You do not have any passkeys registered.',
defaultMessage: 'You have no passkeys registered yet.',
},
managePasskeyModalAdded: {
id: 'settings.account.security.passkey.modal.added',
@@ -303,30 +304,13 @@ const messages = defineMessages({
},
})
const removePasskeyModal = useTemplateRef<InstanceType<typeof ConfirmModal>>('removePasskeyModal')
const managePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('managePasskeyModal')
const addPasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('addPasskeyModal')
const renamePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('renamePasskeyModal')
const managePasskeyModal = ref()
const renamePasskeyModal = ref()
const addPasskeyModal = ref()
const pendingPasskey = ref<Labrinth.Auth.v2.PasskeyRegisterFinishRequest | null>(null)
const passkeyToRemove = ref<Labrinth.Auth.v2.Passkey | null>(null)
const passkeyToRename = ref<Labrinth.Auth.v2.Passkey | null>(null)
const pendingPasskeyName = computed({
get: () => pendingPasskey.value?.name ?? '',
set: (name) => {
if (pendingPasskey.value) {
pendingPasskey.value.name = name
}
},
})
const passkeyToRenameName = computed({
get: () => passkeyToRename.value?.name ?? '',
set: (name) => {
if (passkeyToRename.value) {
passkeyToRename.value.name = name
}
},
})
const passkeys = ref<Labrinth.Auth.v2.Passkey[]>([])
const passkeysLoading = ref(false)
@@ -342,7 +326,7 @@ async function fetchPasskeys() {
}
async function showPasskeyModal() {
managePasskeyModal.value?.show()
managePasskeyModal.value.show()
await fetchPasskeys()
}
@@ -358,7 +342,7 @@ async function registerPasskey() {
name: '',
}
addPasskeyModal.value?.show()
addPasskeyModal.value.show()
} catch (err) {
notifyError(err)
}
@@ -373,7 +357,7 @@ async function finishRegisterPasskey() {
const passkey = await labrinth.auth_v2.registerPasskeyFinish(pendingPasskey.value)
passkeys.value.unshift(passkey)
pendingPasskey.value = null
addPasskeyModal.value?.hide()
addPasskeyModal.value.hide()
} catch (err) {
notifyError(err)
}
@@ -392,7 +376,7 @@ async function renamePasskey() {
existing.name = name
}
passkeyToRename.value = null
renamePasskeyModal.value?.hide()
renamePasskeyModal.value.hide()
} catch (err) {
notifyError(err)
}
@@ -414,3 +398,25 @@ async function removePasskey() {
stopLoading()
}
</script>
<style lang="scss" scoped>
.passkey {
display: flex;
flex-direction: column;
gap: 0.5rem;
@media screen and (min-width: 650px) {
flex-direction: row;
align-items: center;
}
}
.passkey-actions {
flex-direction: row;
@media screen and (min-width: 650px) {
flex-direction: column;
margin-left: auto;
}
}
</style>
+1 -1
View File
@@ -4128,7 +4128,7 @@
"message": "Never used"
},
"settings.account.security.passkey.modal.no-passkeys": {
"message": "You do not have any passkeys registered."
"message": "You have no passkeys registered yet."
},
"settings.account.security.passkey.remove.description": {
"message": "This will permanently remove the passkey \"{name}\". You will no longer be able to sign in with it."
+4 -4
View File
@@ -33,7 +33,7 @@
@keyup.enter="saveEmail()"
/>
</div>
<div class="flex justify-end gap-2.5">
<div class="input-group push-right">
<ButtonStyled>
<button @click="$refs.changeEmailModal.hide()">
<XIcon />
@@ -128,7 +128,7 @@
</div>
</div>
</template>
<div class="flex justify-end gap-2.5">
<div class="input-group push-right">
<ButtonStyled>
<button @click="$refs.managePasswordModal.hide()">
<XIcon />
@@ -201,7 +201,7 @@
<p v-if="twoFactorIncorrect" class="known-errors m-0">
{{ formatMessage(messages.twoFactorIncorrectError) }}
</p>
<div class="flex justify-end gap-2.5">
<div class="input-group push-right">
<ButtonStyled>
<button @click="$refs.manageTwoFactorModal.hide()">
<XIcon />
@@ -279,7 +279,7 @@
<li v-for="code in backupCodes" :key="code">{{ code }}</li>
</ul>
</template>
<div class="flex justify-end gap-2.5">
<div class="input-group push-right">
<ButtonStyled v-if="twoFactorStep === 1">
<button @click="twoFactorStep = 0">
<LeftArrowIcon />
+1 -8
View File
@@ -526,11 +526,4 @@ yahoo.com, 100033, DIRECT, e1a5b5b6e3255540
#IndexExchange
indexexchange.com, 184169, DIRECT, 50b1c356f2c5c8fc
indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc
#AppNexus
Xandr.com, 2398, DIRECT
#Rubicon
rubiconproject.com, 24584, DIRECT, 0bfd66d529a55807
rubiconproject.com, 24586, DIRECT, 0bfd66d529a55807
indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = CASE\n\t\t\t\tWHEN submitted_time_played < 0 THEN ?\n\t\t\t\tWHEN submitted_time_played > ? THEN ?\n\t\t\t\tELSE submitted_time_played + ?\n\t\t\tEND,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = submitted_time_played + ?,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET recent_time_played = recent_time_played + ?, modified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmissing = 0,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\trecent_time_played = CASE\n\t\t\t\tWHEN recent_time_played < 0 THEN ?\n\t\t\t\tWHEN recent_time_played > ? THEN ?\n\t\t\t\tELSE recent_time_played + ?\n\t\t\tEND,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380"
}
-1
View File
@@ -105,7 +105,6 @@ tracing = { workspace = true }
tracing-error = { workspace = true }
tracing-subscriber = { workspace = true, features = ["chrono", "env-filter"] }
url = { workspace = true, features = ["serde"] }
urlencoding = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4"] }
whoami = { workspace = true }
zbus = { workspace = true }
@@ -1,12 +0,0 @@
UPDATE instances
SET
submitted_time_played = CASE
WHEN submitted_time_played < 0 THEN 0
ELSE submitted_time_played
END,
recent_time_played = CASE
WHEN recent_time_played < 0 THEN 0
ELSE recent_time_played
END
WHERE submitted_time_played < 0
OR recent_time_played < 0;
-49
View File
@@ -7,8 +7,6 @@ use crate::{
},
util::io,
};
use url::form_urlencoded;
use urlencoding::decode;
/// Handles external functions (such as through URL deep linkage)
/// Link is extracted value (link) in somewhat URL format, such as
@@ -30,53 +28,6 @@ pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
Some(("server", id)) => {
CommandPayload::InstallServer { id: id.to_string() }
}
// /launch/instance/{id} - Launches an instance
Some(("launch", rest)) if rest.starts_with("instance/") => {
let raw = rest.trim_start_matches("instance/");
let (raw, query) = raw.split_once('?').unwrap_or((raw, ""));
let mut server = None;
let mut singleplayer_world = None;
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
match &*key {
"server" => server = Some(value.into_owned()),
"singleplayer_world" => {
singleplayer_world = Some(value.into_owned());
}
_ => {}
}
}
if server.is_some() && singleplayer_world.is_some() {
emit_warning(
"Invalid command, cannot launch both a server and a singleplayer world",
)
.await?;
return Err(crate::ErrorKind::InputError(
"Cannot launch both a server and a singleplayer world"
.to_string(),
)
.into());
}
match decode(raw) {
Ok(decoded) => CommandPayload::LaunchInstance {
id: decoded.to_string(),
server,
singleplayer_world,
},
Err(e) => {
emit_warning(&format!(
"Invalid UTF-8 in instance path: {e}"
))
.await?;
return Err(crate::ErrorKind::InputError(format!(
"Invalid UTF-8 in instance path: {e}"
))
.into());
}
}
}
_ => {
emit_warning(&format!(
"Invalid command, unrecognized path: {sublink}"
+6 -27
View File
@@ -250,15 +250,9 @@ impl Logs {
output: if clear_contents.unwrap_or(false) {
None
} else {
let state = State::get().await?;
Some(
get_output_by_filename_from_path(
&state,
instance_path,
log_type,
&filename,
)
.await?,
get_output_by_filename(instance_path, log_type, &filename)
.await?,
)
},
filename,
@@ -364,12 +358,14 @@ pub async fn get_logs_by_filename(
Logs::build(log_type, age, &instance_path, filename, Some(true)).await
}
async fn get_output_by_filename_from_path(
state: &State,
#[tracing::instrument]
pub async fn get_output_by_filename(
instance_path: &str,
log_type: LogType,
file_name: &str,
) -> crate::Result<CensoredString> {
let state = State::get().await?;
let logs_folder = match log_type {
LogType::InfoLog => state.directories.instance_logs_dir(instance_path),
LogType::CrashReport => {
@@ -413,23 +409,6 @@ async fn get_output_by_filename_from_path(
.into())
}
#[tracing::instrument]
pub async fn get_output_by_filename(
instance_id: &str,
log_type: LogType,
file_name: &str,
) -> crate::Result<CensoredString> {
let state = State::get().await?;
let instance_path = resolve_instance_path(instance_id, &state).await?;
get_output_by_filename_from_path(
&state,
&instance_path,
log_type,
file_name,
)
.await
}
#[tracing::instrument]
pub async fn delete_logs(instance_id: &str) -> crate::Result<()> {
let state = State::get().await?;
-5
View File
@@ -229,11 +229,6 @@ pub enum CommandPayload {
InstallServer {
id: String,
},
LaunchInstance {
id: String,
server: Option<String>,
singleplayer_world: Option<String>,
},
RunMRPack {
// run or install .mrpack
path: PathBuf,
@@ -117,7 +117,6 @@ impl ModLoader {
pub struct ContentFile {
pub hash: String,
pub file_name: String,
pub enabled: bool,
pub size: u64,
pub metadata: Option<FileMetadata>,
pub update_version_id: Option<String>,
@@ -627,7 +627,6 @@ pub(crate) async fn rename_instance_file(
relative_path = ?,
file_name = ?,
enabled = ?,
missing = 0,
modified_at = ?
WHERE instance_id = ? AND relative_path = ?
",
@@ -857,11 +856,11 @@ pub(crate) async fn set_content_entry_enabled_for_file(
file_id: &str,
enabled: bool,
pool: &SqlitePool,
) -> crate::Result<bool> {
) -> crate::Result<()> {
let enabled = i64::from(enabled);
let modified_at = Utc::now().timestamp();
let result = sqlx::query!(
sqlx::query!(
"
UPDATE instance_content_entries
SET enabled = ?, modified_at = ?
@@ -875,7 +874,7 @@ pub(crate) async fn set_content_entry_enabled_for_file(
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
Ok(())
}
pub(crate) async fn remove_content_entries_for_file(
@@ -3,7 +3,7 @@
use crate::state::instances::{
ContentSet, ContentSetStatus, ContentSourceKind, Instance,
InstanceLaunchContext, InstanceLaunchOverrides,
InstanceLaunchOverridesData, InstanceLink, playtime_to_storage,
InstanceLaunchOverridesData, InstanceLink,
};
use crate::state::{
InstanceInstallStage, LauncherFeatureVersion, ModLoader, ReleaseChannel,
@@ -831,12 +831,8 @@ pub(crate) async fn insert_instance(
let created = instance.created.timestamp();
let modified = instance.modified.timestamp();
let last_played = instance.last_played.map(|value| value.timestamp());
let submitted_time_played = playtime_to_storage(
instance.submitted_time_played,
"submitted_time_played",
)?;
let recent_time_played =
playtime_to_storage(instance.recent_time_played, "recent_time_played")?;
let submitted_time_played = instance.submitted_time_played as i64;
let recent_time_played = instance.recent_time_played as i64;
sqlx::query!(
"
@@ -891,12 +887,8 @@ pub(crate) async fn update_instance(
let icon_path = instance.icon_path.as_deref();
let modified = instance.modified.timestamp();
let last_played = instance.last_played.map(|value| value.timestamp());
let submitted_time_played = playtime_to_storage(
instance.submitted_time_played,
"submitted_time_played",
)?;
let recent_time_played =
playtime_to_storage(instance.recent_time_played, "recent_time_played")?;
let submitted_time_played = instance.submitted_time_played as i64;
let recent_time_played = instance.recent_time_played as i64;
sqlx::query!(
"
@@ -661,31 +661,13 @@ pub(crate) async fn toggle_disable_project(
}
None => index_existing_file(&scope, &new_path, state).await?,
};
let updated_entry = content_rows::set_content_entry_enabled_for_file(
content_rows::set_content_entry_enabled_for_file(
&scope.content_set_id,
&file.id,
enabled,
&state.pool,
)
.await?;
if !updated_entry {
let project_type = ProjectType::get_from_parent_folder(&new_path)
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Unable to infer project type from {new_path}"
))
})?;
upsert_entry_for_file(
&scope,
&file,
project_type,
None,
None,
ContentSourceKind::Local,
&state.pool,
)
.await?;
}
Ok(new_path)
}
@@ -281,31 +281,12 @@ async fn plan_bulk_update(
instance_id: &str,
state: &State,
) -> crate::Result<BulkUpdatePlan> {
let updateable_paths =
bulk_updateable_project_paths(instance_id, state).await?;
if updateable_paths.is_empty() {
return Ok(BulkUpdatePlan {
project_updates: Vec::new(),
dependency_additions: Vec::new(),
});
}
let updates = check_content_updates(
instance_id,
Some(CacheBehaviour::MustRevalidate),
state,
)
.await?
.into_iter()
.filter(|update| updateable_paths.contains(&update.relative_path))
.collect::<Vec<_>>();
if updates.is_empty() {
return Ok(BulkUpdatePlan {
project_updates: Vec::new(),
dependency_additions: Vec::new(),
});
}
.await?;
let content_set =
content_rows::get_applied_content_set(instance_id, &state.pool)
.await?
@@ -339,7 +320,6 @@ async fn plan_bulk_update(
.collect::<HashMap<_, _>>();
let version_ids = installed
.iter()
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| project.version_id.clone())
.chain(
updates
@@ -363,7 +343,6 @@ async fn plan_bulk_update(
let planned_versions = installed
.iter()
.filter(|project| project.enabled)
.filter(|project| updateable_paths.contains(&project.relative_path))
.filter_map(|project| {
let target_version_id = updates_by_path
.get(&project.relative_path)
@@ -400,21 +379,6 @@ async fn plan_bulk_update(
})
}
async fn bulk_updateable_project_paths(
instance_id: &str,
state: &State,
) -> crate::Result<HashSet<String>> {
let items = super::list_content::list_content(
instance_id,
None,
Some(CacheBehaviour::MustRevalidate),
state,
)
.await?;
Ok(items.into_iter().map(|item| item.file_path).collect())
}
async fn installed_projects(
instance_id: &str,
content_set: &ContentSet,
@@ -1,6 +1,6 @@
use crate::state::InstanceInstallStage;
use crate::state::instances::{
InstanceLaunchContext, adapters::sqlite::instance_rows, playtime_to_storage,
InstanceLaunchContext, adapters::sqlite::instance_rows,
};
use chrono::{DateTime, Utc};
use sqlx::SqlitePool;
@@ -120,31 +120,16 @@ pub(crate) async fn add_instance_recent_playtime(
seconds: u64,
pool: &SqlitePool,
) -> crate::Result<()> {
if seconds == 0 {
return Ok(());
}
let seconds = playtime_to_storage(seconds, "recent_time_played")?;
let max_playtime = i64::MAX;
let max_playtime_before_increment = max_playtime - seconds;
let seconds = seconds as i64;
let modified = Utc::now().timestamp();
sqlx::query!(
"
UPDATE instances
SET
recent_time_played = CASE
WHEN recent_time_played < 0 THEN ?
WHEN recent_time_played > ? THEN ?
ELSE recent_time_played + ?
END,
modified = ?
SET recent_time_played = recent_time_played + ?, modified = ?
WHERE id = ?
",
seconds,
max_playtime_before_increment,
max_playtime,
seconds,
modified,
instance_id,
)
@@ -159,33 +144,19 @@ pub(crate) async fn mark_instance_playtime_submitted(
recent_time_played: u64,
pool: &SqlitePool,
) -> crate::Result<()> {
if recent_time_played == 0 {
return Ok(());
}
let recent_time_played =
playtime_to_storage(recent_time_played, "recent_time_played")?;
let max_playtime = i64::MAX;
let max_playtime_before_increment = max_playtime - recent_time_played;
let recent_time_played = recent_time_played as i64;
let modified = Utc::now().timestamp();
sqlx::query!(
"
UPDATE instances
SET
submitted_time_played = CASE
WHEN submitted_time_played < 0 THEN ?
WHEN submitted_time_played > ? THEN ?
ELSE submitted_time_played + ?
END,
submitted_time_played = submitted_time_played + ?,
recent_time_played = 0,
modified = ?
WHERE id = ?
",
recent_time_played,
max_playtime_before_increment,
max_playtime,
recent_time_played,
modified,
instance_id,
)
@@ -208,8 +208,6 @@ pub(crate) async fn list_content(
)
.await?;
let imported_modpack_scope = is_imported_modpack_scope(&resolved, &link);
let linked_modpack_source_kind = linked_modpack_source_kind(&link);
let mut failed_modpack_identifier_lookup = false;
let modpack_ids = if imported_modpack_scope {
None
} else {
@@ -228,7 +226,6 @@ pub(crate) async fn list_content(
"Failed to fetch modpack identifiers: {}",
err
);
failed_modpack_identifier_lookup = true;
None
}
},
@@ -243,12 +240,6 @@ pub(crate) async fn list_content(
}
} else if let Some(ids) = modpack_ids.as_ref() {
ContentFilter::ExcludeModpack(ids)
} else if failed_modpack_identifier_lookup {
ContentFilter::ExcludeSourceKind {
source_kind: linked_modpack_source_kind
.unwrap_or(ContentSourceKind::ModrinthModpack),
exclude_untracked: true,
}
} else {
ContentFilter::All
};
@@ -744,9 +735,6 @@ async fn content_projects_for_scope(
update_version_id,
hash: file.sha1,
file_name: file.file_name,
enabled: entry.map_or(file.enabled, |entry| {
entry.enabled && file.enabled
}),
size: file.size,
metadata: file_metadata_from_entry_or_cache(entry, metadata),
project_type,
@@ -894,7 +882,7 @@ async fn content_files_to_content_items(
file_path: path.clone(),
id: file.hash.clone(),
size: file.size,
enabled: file.enabled,
enabled: !file.file_name.ends_with(".disabled"),
project_type: file.project_type,
project: project.map(|project| ContentItemProject {
id: project.id.clone(),
@@ -1109,20 +1097,6 @@ fn linked_modpack_ids(link: &InstanceLink) -> Option<(String, String)> {
}
}
fn linked_modpack_source_kind(
link: &InstanceLink,
) -> Option<ContentSourceKind> {
match link {
InstanceLink::ModrinthModpack { .. } => {
Some(ContentSourceKind::ModrinthModpack)
}
InstanceLink::ServerProjectModpack { .. } => {
Some(ContentSourceKind::ServerProject)
}
_ => None,
}
}
fn check_modpack_update(
installed_version_id: &str,
installed_version: &Version,
@@ -39,18 +39,9 @@ pub(crate) async fn sync_instance_content_files(
&state.api_semaphore,
)
.await?;
let hashes_by_key = hashes
let hashes_by_path = hashes
.into_iter()
.map(|hash| {
(
format!(
"{}-{}",
hash.size,
hash.path.trim_end_matches(".disabled")
),
hash,
)
})
.map(|hash| (hash.path.clone(), hash))
.collect::<HashMap<_, _>>();
let existing_files =
sqlite::content_rows::get_instance_files(&instance.id, &state.pool)
@@ -64,8 +55,8 @@ pub(crate) async fn sync_instance_content_files(
let mut files = Vec::new();
for file in scanned {
let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
let Some(hash) = hashes_by_key.get(hash_key) else {
let cache_path = format!("{}/{}", instance.path, file.relative_path);
let Some(hash) = hashes_by_path.get(&cache_path) else {
continue;
};
let existing_file = existing_files_by_path.get(&file.relative_path);
@@ -20,15 +20,3 @@ pub struct Instance {
pub submitted_time_played: u64,
pub recent_time_played: u64,
}
pub(crate) fn playtime_to_storage(
value: u64,
column: &str,
) -> crate::Result<i64> {
i64::try_from(value).map_err(|_| {
crate::ErrorKind::InputError(format!(
"Expected {column} to fit in SQLite INTEGER"
))
.into()
})
}
@@ -3,7 +3,7 @@ use crate::jre::check_jre;
use crate::prelude::ModLoader;
use crate::state;
use crate::state::instances::{
InstanceLaunchOverrides, InstanceLaunchOverridesData, playtime_to_storage,
InstanceLaunchOverrides, InstanceLaunchOverridesData,
};
use crate::state::{
CacheValue, CachedEntry, CachedFile, CachedFileHash, CachedFileUpdate,
@@ -443,12 +443,8 @@ where
let created = input.created.timestamp();
let modified = input.modified.timestamp();
let last_played = input.last_played.map(|value| value.timestamp());
let submitted_time_played = playtime_to_storage(
input.submitted_time_played,
"submitted_time_played",
)?;
let recent_time_played =
playtime_to_storage(input.recent_time_played, "recent_time_played")?;
let submitted_time_played = input.submitted_time_played as i64;
let recent_time_played = input.recent_time_played as i64;
sqlx::query!(
"
+27 -28
View File
@@ -17,7 +17,6 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::sync::LazyLock;
use std::time::Instant;
#[cfg(feature = "tauri")]
use tauri::Emitter;
use tempfile::TempDir;
@@ -753,46 +752,46 @@ impl Process {
uuid: Uuid,
) -> crate::Result<()> {
async fn update_playtime(
last_updated_playtime: &mut Instant,
last_updated_playtime: &mut DateTime<Utc>,
instance_id: &str,
force_update: bool,
) {
let elapsed = last_updated_playtime.elapsed().as_secs();
if elapsed == 0 || (!force_update && elapsed < 60) {
return;
}
let state = match crate::State::get().await {
Ok(state) => state,
Err(e) => {
let diff = Utc::now()
.signed_duration_since(*last_updated_playtime)
.num_seconds();
if diff >= 60 || force_update {
let state = match crate::State::get().await {
Ok(state) => state,
Err(e) => {
tracing::warn!(
"Failed to get state for playtime update on instance {}: {}",
instance_id,
e
);
return;
}
};
if let Err(e) =
crate::state::instances::commands::add_instance_recent_playtime(
instance_id,
diff as u64,
&state.pool,
)
.await
{
tracing::warn!(
"Failed to get state for playtime update on instance {}: {}",
"Failed to update playtime for instance {}: {}",
instance_id,
e
);
return;
}
};
if let Err(e) =
crate::state::instances::commands::add_instance_recent_playtime(
instance_id,
elapsed,
&state.pool,
)
.await
{
tracing::warn!(
"Failed to update playtime for instance {}: {}",
instance_id,
e
);
*last_updated_playtime = Utc::now();
}
*last_updated_playtime = Instant::now();
}
// Wait on current Minecraft Child
let mc_exit_status;
let mut last_updated_playtime = Instant::now();
let mut last_updated_playtime = Utc::now();
let state = crate::State::get().await?;
loop {
-1
View File
@@ -63,7 +63,6 @@ pub enum FeatureFlag {
ServerProjectQa,
I18nDebug,
ShowInstancePlayTime,
SkipNonEssentialWarnings,
}
impl Settings {
-39
View File
@@ -10,45 +10,6 @@ export type VersionEntry = {
}
const VERSIONS: VersionEntry[] = [
{
date: `2026-06-25T21:22:14+00:00`,
product: 'app',
version: '0.15.0',
body: `## Added
- Existing app data is now backed up after the app updates.
- Added an option to open the folder which contains the app database backups in the Resource management tab in the app settings.
- Added an option to skip non-essential warnings across the app.
- Added the ability to create desktop shortcuts for your instances.
## Changed
- Instance installs in the Modrinth App now show detailed progress for preparing Java, downloading modpack files, downloading content, extracting overrides, and downloading Minecraft.
- Java, modpack, content, overrides and Minecraft downloads/extracting now show downloaded/extracted and total size.
- The Content tabs "Update all" action now shows live progress while resolving versions, downloading projects, and finishing updates.
- Changed the visual style of the Delete server/Delete world modal on the Worlds tab of instances to match the Content tab's modals.
## Fixed
- Fixed broken Delete server modal on the Worlds tab of instances.
- Fixed the navigation bars across the app not animating when clicking between pages.
- Fixed fade in animation incorrectly playing on the Mods page even when the instance content is already loaded into the UI.
- Failed or interrupted instance installs now roll back partial changes instead of leaving incomplete new instances or modified/broken existing instances behind.
- Failed or interrupted instance installs now explain why the failure or interruption occurred, and provide a retry action.
- The Home page, Library page, and the quick instance switcher in the left sidebar now correctly refresh when an instance begins or finishes installing.
- Fixed issue where going from an instance page to the browse page via the left sidebar would cause the app to completely freeze up.
- Fixed issue where using the "Add to instance" button on search results in the browse page would take a long time to process if you have a lot of instances.
- Fixed many performance issues with loading/editing of instances.`,
},
{
date: `2026-06-25T21:22:14+00:00`,
product: 'web',
body: `## Added
- Create account page where user can input username for new account.
- Log in with passkeys support, thanks [@DeDiamondPro](https://github.com/DeDiamondPro)!
## Changed
- Refreshed sign in and sign up page styles.
- Refreshed forget password and verify email pages.
- Updated account settings modals to match website styles for consistency.`,
},
{
date: `2026-06-24T00:30:00-07:00`,
product: 'web',
+103 -116
View File
@@ -148,135 +148,122 @@ defineExpose({
<template>
<NewModal ref="shareModal" :header="header" :noblur="noblur" :on-hide="onHide">
<div class="flex flex-col items-center gap-2">
<div
:class="['flex items-center justify-center', link ? 'flex-wrap gap-4' : 'flex-col gap-2']"
<div class="flex flex-col flex-wrap items-center gap-2">
<div v-if="link" class="group relative mx-auto">
<div ref="qrCode">
<QrcodeVue :value="url" class="!bg-white rounded-[var(--radius-md)]" margin="3" />
</div>
<ButtonStyled circular type="transparent">
<button
v-tooltip="'Copy QR code'"
class="absolute top-0 right-0 m-2"
aria-label="Copy QR code"
@click="copyImage"
>
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</button>
</ButtonStyled>
</div>
<StyledInput
v-else
v-model="content"
multiline
resize="vertical"
wrapper-class="h-full w-[30rem]"
>
<div v-if="link" class="group relative shrink-0">
<div ref="qrCode">
<QrcodeVue :value="url" class="!bg-white rounded-[var(--radius-md)]" margin="3" />
</div>
<template #right>
<ButtonStyled circular type="transparent">
<button
v-tooltip="'Copy QR code'"
v-tooltip="'Copy Text'"
type="button"
aria-label="Copy Text"
class="absolute top-0 right-0 m-2"
aria-label="Copy QR code"
@click="copyImage"
@click="copyText"
>
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</button>
</ButtonStyled>
</div>
<StyledInput
v-else
v-model="content"
multiline
resize="vertical"
wrapper-class="h-full w-[30rem]"
</template>
</StyledInput>
<div class="flex flex-grow flex-col justify-center gap-2">
<button
v-if="link"
v-tooltip="'Copy Link'"
type="button"
aria-label="Copy Link"
class="flex h-10 w-full cursor-pointer items-center justify-between gap-2 rounded-xl border-none bg-button-bg px-3 pr-1.5 text-primary transition-all hover:bg-button-bg-hover hover:brightness-125 active:scale-95"
@click="copyText"
>
<template #right>
<ButtonStyled circular type="transparent">
<button
v-tooltip="'Copy Text'"
type="button"
aria-label="Copy Text"
class="absolute top-0 right-0 m-2"
@click="copyText"
>
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</button>
</ButtonStyled>
</template>
</StyledInput>
<div
v-if="link || socialButtons"
:class="['flex flex-col justify-center gap-2', link ? 'w-64 max-w-full' : 'flex-grow']"
>
<button
v-if="link"
v-tooltip="'Copy Link'"
type="button"
aria-label="Copy Link"
class="flex h-10 w-full cursor-pointer items-center justify-between gap-2 rounded-xl border-none bg-button-bg px-3 pr-1.5 text-primary transition-all hover:bg-button-bg-hover hover:brightness-125 active:scale-95"
@click="copyText"
>
<span class="min-w-0 cursor-pointer truncate text-left font-semibold text-primary">
{{ url }}
</span>
<div class="grid h-10 w-10 place-content-center">
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</div>
</button>
<ButtonStyled v-if="link">
<span class="cursor-pointer truncate text-left font-semibold text-primary">
{{ url }}
</span>
<div class="grid h-10 w-10 place-content-center">
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</div>
</button>
<ButtonStyled v-if="link">
<a :href="url" target="_blank" rel="noopener noreferrer" aria-label="Open in new tab">
Open in new tab
<ExternalIcon aria-hidden="true" />
</a>
</ButtonStyled>
<div v-if="socialButtons" class="flex flex-row gap-1">
<ButtonStyled v-if="canShare" circular>
<button v-tooltip="'Share'" aria-label="Share" @click="share">
<ShareIcon aria-hidden="true" />
</button>
</ButtonStyled>
<ButtonStyled circular>
<a
:href="url"
target="_blank"
rel="noopener noreferrer"
aria-label="Open in new tab"
class="w-full"
v-tooltip="'Send as an email'"
:href="sendEmail"
:target="targetParameter"
aria-label="Send as an email"
>
Open in new tab
<ExternalIcon aria-hidden="true" />
<MailIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-if="link"
v-tooltip="'Open link in browser'"
:target="targetParameter"
:href="url"
aria-label="Open link in browser"
>
<GlobeIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-tooltip="'Toot about it'"
:target="targetParameter"
:href="sendToot"
aria-label="Toot about it"
>
<MastodonIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-tooltip="'Tweet about it'"
:target="targetParameter"
:href="sendTweet"
aria-label="Tweet about it"
>
<TwitterIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-tooltip="'Share on Reddit'"
:target="targetParameter"
:href="postOnReddit"
aria-label="Share on Reddit"
>
<RedditIcon aria-hidden="true" />
</a>
</ButtonStyled>
<div v-if="socialButtons" class="flex flex-row gap-1">
<ButtonStyled v-if="canShare" circular>
<button v-tooltip="'Share'" aria-label="Share" @click="share">
<ShareIcon aria-hidden="true" />
</button>
</ButtonStyled>
<ButtonStyled circular>
<a
v-tooltip="'Send as an email'"
:href="sendEmail"
:target="targetParameter"
aria-label="Send as an email"
>
<MailIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-if="link"
v-tooltip="'Open link in browser'"
:target="targetParameter"
:href="url"
aria-label="Open link in browser"
>
<GlobeIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-tooltip="'Toot about it'"
:target="targetParameter"
:href="sendToot"
aria-label="Toot about it"
>
<MastodonIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-tooltip="'Tweet about it'"
:target="targetParameter"
:href="sendTweet"
aria-label="Tweet about it"
>
<TwitterIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular>
<a
v-tooltip="'Share on Reddit'"
:target="targetParameter"
:href="postOnReddit"
aria-label="Share on Reddit"
>
<RedditIcon aria-hidden="true" />
</a>
</ButtonStyled>
</div>
</div>
</div>
</div>
@@ -55,11 +55,6 @@ async function handleBack() {
if (!context) return
if (selectedCount.value > 0 && !isInstallingSelected.value) {
if (context.skipNonEssentialWarnings) {
await handleSelectedProjectsLeaveResult('discard', context)
return
}
const result = await selectedProjectsLeaveModal.value?.prompt()
await handleSelectedProjectsLeaveResult(result ?? 'cancel', context)
return
@@ -36,7 +36,6 @@ export interface BrowseInstallContext {
onBack?: () => boolean | void | Promise<boolean | void>
selectedProjects?: BrowseSelectedProject[]
isInstallingSelected?: boolean
skipNonEssentialWarnings?: boolean
installProgress?: {
completed: number
total: number
@@ -151,7 +151,6 @@ const messages = defineMessages({
})
const ctx = injectContentManager()
const skipNonEssentialWarnings = computed(() => ctx.skipNonEssentialWarnings?.value ?? false)
function getItemId(item: ContentItem) {
return ctx.getItemId?.(item) ?? item.file_path ?? item.file_name ?? item.id
@@ -390,7 +389,7 @@ async function promptDeleteItems(items: ContentItem[], event?: MouseEvent) {
}
function showDeletionConfirmation(event?: MouseEvent) {
if ((event?.shiftKey || skipNonEssentialWarnings.value) && !ctx.isBusy.value) {
if (event?.shiftKey && !ctx.isBusy.value) {
confirmDelete()
} else {
confirmDeletionModal.value?.show()
@@ -582,7 +581,7 @@ function promptUpdateAll(event?: MouseEvent) {
if (items.length === 0) return
pendingBulkUpdateItems.value = items
pendingBulkUpdateAll.value = true
if ((event?.shiftKey || skipNonEssentialWarnings.value) && !ctx.isBusy.value) {
if (event?.shiftKey && !ctx.isBusy.value) {
confirmBulkUpdate()
} else {
confirmBulkUpdateModal.value?.show()
@@ -595,7 +594,7 @@ function promptUpdateSelected(event?: MouseEvent) {
if (items.length === 0) return
pendingBulkUpdateItems.value = items
pendingBulkUpdateAll.value = false
if ((event?.shiftKey || skipNonEssentialWarnings.value) && !ctx.isBusy.value) {
if (event?.shiftKey && !ctx.isBusy.value) {
confirmBulkUpdate()
} else {
confirmBulkUpdateModal.value?.show()
@@ -47,7 +47,6 @@ export interface ContentManagerContext {
// Guards
isBusy: Ref<boolean> | ComputedRef<boolean>
busyMessage?: Ref<string | null> | ComputedRef<string | null>
skipNonEssentialWarnings?: Ref<boolean> | ComputedRef<boolean>
disableAddContent?: Ref<boolean> | ComputedRef<boolean>
disableAddContentTooltip?: string
@@ -43,7 +43,6 @@ import type { LoaderVersionEntry } from './types'
const { formatMessage } = useVIntl()
const ctx = injectInstallationSettings()
const debug = useDebugLogger('InstallationSettingsLayout')
const skipNonEssentialWarnings = computed(() => ctx.skipNonEssentialWarnings?.value ?? false)
const availablePlatforms = computed(() =>
Array.isArray(ctx.availablePlatforms) ? ctx.availablePlatforms : ctx.availablePlatforms.value,
)
@@ -229,7 +228,7 @@ function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version, event
isUpdateDowngrade.value ||
versionChangesGameVersion(version, ctx.updaterModalProps.value.currentGameVersion)
if (event?.shiftKey || skipNonEssentialWarnings.value || !shouldShowWarning) {
if (event?.shiftKey || !shouldShowWarning) {
debug('handleModpackUpdateRequest: confirming without warning', {
isUpdateDowngrade: isUpdateDowngrade.value,
shouldShowWarning,
@@ -372,10 +371,6 @@ function handleShowRepairModal() {
snapshot: stateSnapshot(),
refs: modalRefsSnapshot(),
})
if (skipNonEssentialWarnings.value) {
handleRepair()
return
}
repairModal.value?.show()
nextTick(() => {
debug('handleShowRepairModal: after nextTick', {
@@ -391,7 +386,7 @@ function handleShowUnlinkModal(event: MouseEvent) {
snapshot: stateSnapshot(),
refs: modalRefsSnapshot(),
})
if (event.shiftKey || skipNonEssentialWarnings.value) {
if (event.shiftKey) {
handleUnlink()
return
}
@@ -17,7 +17,6 @@ export interface InstallationSettingsContext {
isLinked: ComputedRef<boolean>
isBusy: Ref<boolean> | ComputedRef<boolean>
busyMessage?: Ref<string | null> | ComputedRef<string | null>
skipNonEssentialWarnings?: Ref<boolean> | ComputedRef<boolean>
modpack: Ref<InstallationModpackData | null> | ComputedRef<InstallationModpackData | null>
+1 -1
View File
@@ -237,7 +237,7 @@
"defaultMessage": "Re-install modpack"
},
"button.remove": {
"defaultMessage": "Delete passkey"
"defaultMessage": "Remove"
},
"button.remove-image": {
"defaultMessage": "Remove image"
+1 -1
View File
@@ -245,7 +245,7 @@ export const commonMessages = defineMessages({
},
removeButton: {
id: 'button.remove',
defaultMessage: 'Delete passkey',
defaultMessage: 'Remove',
},
removeImageButton: {
id: 'button.remove-image',