Compare commits

..

7 Commits

Author SHA1 Message Date
Wyatt Verchere
6e147cc9db fmt clippy prettier 2024-01-30 19:47:44 -08:00
Wyatt Verchere
d2834ce720 Merge branch 'shared-profiles-backend' of http://github.com/modrinth/theseus into shared-profiles-backend 2024-01-30 19:44:12 -08:00
Wyatt Verchere
f9beea8ef2 fmt clippy prettier 2024-01-30 19:39:46 -08:00
thesuzerain
1018d05e36 working acceptance + download 2024-01-30 19:14:47 -08:00
Wyatt Verchere
719aded698 removing users 2024-01-30 19:13:22 -08:00
Wyatt Verchere
6b99f82cea api modifications 2024-01-30 17:42:58 -08:00
Wyatt Verchere
bdde054036 first draft 2024-01-30 17:35:13 -08:00
2004 changed files with 30542 additions and 233587 deletions

View File

@@ -1,3 +1,3 @@
# Windows has stack overflows when calling from Tauri, so we increase compiler size
[target.'cfg(windows)']
rustflags = ["-C", "link-args=/STACK:16777220"]
rustflags = ["-C", "link-args=/STACK:16777220"]

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
* text=auto eol=lf

View File

@@ -1,59 +0,0 @@
name: 🎮 Modrinth App bug
description: Report an issue in the Modrinth Launcher.
labels: [bug, app]
body:
- type: checkboxes
attributes:
label: Please confirm the following.
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate problems
required: true
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
required: true
- label: I have ensured my Modrinth App installation is up to date
required: true
- type: input
id: version
attributes:
label: What version of the Modrinth App are you using?
description: Find this in ⚙️ Settings (bottom right) -> About -> App version.
validations:
required: true
- type: dropdown
id: oses
attributes:
label: What operating systems are you seeing the problem on?
multiple: true
options:
- Windows
- MacOS
- Linux
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. Include screenshots if applicable.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: false
- type: textarea
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

View File

@@ -1,52 +0,0 @@
name: 🌐 Website bug (modrinth.com)
description: Report an issue on the Modrinth website.
labels: [bug, web]
body:
- type: checkboxes
attributes:
label: Please confirm the following.
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate problems
required: true
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Chrome (including Arc, Brave, Opera, Vivaldi)
- Microsoft Edge
- Firefox
- Safari
- Other (please specify)
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. Include screenshots if applicable.
validations:
required: false
- type: textarea
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: false
- type: textarea
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here.
validations:
required: false

View File

@@ -1,19 +1,11 @@
name: 🛠️ API issue (api.modrinth.com)
description: Report an issue regarding the Modrinth API.
labels: [bug, backend]
name: Bug report
description: Create a report to help us improve Modrinth
labels: [bug]
body:
- type: checkboxes
attributes:
label: Please confirm the following.
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate problems
required: true
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. Include screenshots if applicable.
description: A clear and concise description of what the bug is.
validations:
required: false
- type: textarea
@@ -33,9 +25,13 @@ body:
description: A clear and concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: System information
description: Add any information about what OS you are on (like Windows or Mac), and what version of the app you are using.
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here.
description: Add any other context about the problem here. This might include logs, screenshots, etc. The more the merrier!
validations:
required: false

View File

@@ -1,14 +1,16 @@
blank_issues_enabled: true
contact_links:
- name: 🫶 Support Portal
about: Get support using through our portal.
url: https://support.modrinth.com
- name: 💬 Chat
about: Join our Discord server to chat about Modrinth.
- name: Discord
about: Ask questions on our Discord Server.
url: https://discord.modrinth.com
- name: 🛣️ Roadmap
- name: Roadmap
about: View our Roadmap. Please do not open issues for items on our roadmap.
url: https://roadmap.modrinth.com
- name: 📚 Documentation
about: Useful documentation about Modrinth's API
- name: Discussions (Feature requests)
about: |
Please use Issues for reporting bugs and suggesting enhancements to existing features.
Suggestions for new features should be made as a Discussion.
url: https://github.com/orgs/modrinth/discussions/categories/feature-requests
- name: Documentation
about: Useful documentation about Modrinth, its API, and how you can contribute.
url: https://docs.modrinth.com

View File

@@ -1,28 +1,10 @@
name: 💡 Feature Request
description: Suggest an idea
name: Enhancement
description: Suggest an enhancement for an existing Modrinth feature
labels: [enhancement]
body:
- type: checkboxes
attributes:
label: Please confirm the following.
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests
required: true
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
required: true
- type: dropdown
id: projects
attributes:
label: What parts of Modrinth is your feature request related too?
multiple: true
options:
- App
- Website
- API
- type: textarea
attributes:
label: Is your suggested feature related to a problem? Please describe.
label: Is your suggested enhancement related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

44
.github/workflows/cli-build.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: CLI Build + Lint
on:
push:
branches: [ master ]
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./theseus_cli
steps:
- name: Checkout
uses: actions/checkout@v3
- name: install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- uses: actions-rs/cargo@v1
name: Build program
with:
command: build
args: --bin theseus_cli
- name: Run Lint
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --bin theseus_cli

View File

@@ -1,42 +0,0 @@
name: daedalus-docker-build
on:
push:
branches:
- '**'
paths:
- .github/workflows/daedalus-docker.yml
- 'apps/daedalus_client/**'
pull_request:
types: [opened, synchronize]
paths:
- .github/workflows/daedalus-docker.yml
- 'apps/daedalus_client/**'
merge_group:
types: [checks_requested]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Fetch docker metadata
id: docker_meta
uses: docker/metadata-action@v3
with:
images: ghcr.io/modrinth/daedalus
- name: Login to GitHub Images
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
file: ./apps/daedalus_client/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View File

@@ -1,51 +0,0 @@
name: Run Meta
on:
schedule:
- cron: '*/5 * * * *'
workflow_dispatch:
jobs:
run-docker:
if: github.repository_owner == 'modrinth'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker image from GHCR
run: docker pull ghcr.io/modrinth/daedalus:main
- name: Run Docker container
env:
BASE_URL: ${{ secrets.BASE_URL }}
S3_ACCESS_TOKEN: ${{ secrets.S3_ACCESS_TOKEN }}
S3_SECRET: ${{ secrets.S3_SECRET }}
S3_URL: ${{ secrets.S3_URL }}
S3_REGION: ${{ secrets.S3_REGION }}
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
CLOUDFLARE_INTEGRATION: ${{ secrets.CLOUDFLARE_INTEGRATION }}
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
run: |
docker run \
--name daedalus \
-e RUST_LOG=warn,daedalus_client=trace \
-e BASE_URL=$BASE_URL \
-e S3_ACCESS_TOKEN=$S3_ACCESS_TOKEN \
-e S3_SECRET=$S3_SECRET \
-e S3_URL=$S3_URL \
-e S3_REGION=$S3_REGION \
-e S3_BUCKET_NAME=$S3_BUCKET_NAME \
-e CLOUDFLARE_INTEGRATION=$CLOUDFLARE_INTEGRATION \
-e CLOUDFLARE_TOKEN=$CLOUDFLARE_TOKEN \
-e CLOUDFLARE_ZONE_ID=$CLOUDFLARE_ZONE_ID \
ghcr.io/modrinth/daedalus:main

View File

@@ -1,30 +0,0 @@
name: Clear pages cache
on:
push:
branches:
- prod
jobs:
wait:
if: github.repository_owner == 'modrinth'
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- name: Cloudflare Pages deployment
uses: WalshyDev/cf-pages-await@v1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: '9ddae624c98677d68d93df6e524a6061'
project: 'frontend'
commitHash: ${{ steps.push-changes.outputs.commit-hash }}
- name: Purge cache
if: github.ref == 'refs/heads/prod'
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{"hosts": ["modrinth.com", "www.modrinth.com"]}' \
https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache

42
.github/workflows/gui-build.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: GUI Build + Lint
on:
push:
branches: [master]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./theseus_gui
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Install pnpm via corepack
shell: bash
run: |
corepack enable
corepack prepare --activate
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Run Lint
run: pnpm lint
- name: Build
run: pnpm build

View File

@@ -1,45 +0,0 @@
name: docker-build
on:
push:
branches:
- '**'
paths:
- .github/workflows/labrinth-docker.yml
- 'apps/labrinth/**'
pull_request:
types: [opened, synchronize]
paths:
- .github/workflows/labrinth-docker.yml
- 'apps/labrinth/**'
merge_group:
types: [checks_requested]
jobs:
docker:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./apps/labrinth
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Fetch docker metadata
id: docker_meta
uses: docker/metadata-action@v3
with:
images: ghcr.io/modrinth/labrinth
- name: Login to GitHub Images
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: ./apps/labrinth
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

109
.github/workflows/tauri-build.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: 'Tauri GUI Build'
on:
push:
branches: [ master ]
pull_request:
jobs:
test-tauri:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, windows-latest, ubuntu-20.04, ubuntu-22.04]
runs-on: ${{ matrix.platform }}
defaults:
run:
working-directory: ./theseus_gui
steps:
- uses: actions/checkout@v3
- name: Rust setup (mac)
if: startsWith(matrix.platform, 'macos')
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: aarch64-apple-darwin
- name: Rust setup
if: "!startsWith(matrix.platform, 'macos')"
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Install pnpm via corepack
shell: bash
run: |
corepack enable
corepack prepare --activate
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: install dependencies (ubuntu only)
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libselinux1
- name: Install frontend dependencies
run: pnpm install
- name: build app (macos)
uses: tauri-apps/tauri-action@v0
id: build_os_mac
if: startsWith(matrix.platform, 'macos')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
args: --target universal-apple-darwin
- name: build app
uses: tauri-apps/tauri-action@v0
id: build_os
if: "!startsWith(matrix.platform, 'macos')"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v3
if: startsWith(matrix.platform, 'macos')
with:
name: ${{ matrix.platform }}
path: "${{ join(fromJSON(steps.build_os_mac.outputs.artifactPaths), '\n') }}"
- name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v3
if: "!startsWith(matrix.platform, 'macos')"
with:
name: ${{ matrix.platform }}
path: "${{ join(fromJSON(steps.build_os.outputs.artifactPaths), '\n') }}"

View File

@@ -1,155 +0,0 @@
name: 'Modrinth App build'
on:
push:
branches:
- main
tags:
- 'v*'
paths:
- .github/workflows/app-release.yml
- 'apps/app/**'
- 'apps/app-frontend/**'
- 'packages/app-lib/**'
- 'packages/app-macros/**'
- 'packages/assets/**'
- 'packages/ui/**'
- 'packages/utils/**'
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, windows-latest, ubuntu-22.04]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Rust setup (mac)
if: startsWith(matrix.platform, 'macos')
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: aarch64-apple-darwin, x86_64-apple-darwin
- name: Rust setup
if: "!startsWith(matrix.platform, 'macos')"
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Setup rust cache
uses: actions/cache@v4
with:
path: |
target/**
!target/*/release/bundle/*/*.dmg
!target/*/release/bundle/*/*.app.tar.gz
!target/*/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.dmg
!target/release/bundle/*/*.app.tar.gz
!target/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/appimage/*.AppImage
!target/release/bundle/appimage/*.AppImage.tar.gz
!target/release/bundle/appimage/*.AppImage.tar.gz.sig
!target/release/bundle/deb/*.deb
!target/release/bundle/rpm/*.rpm
!target/release/bundle/msi/*.msi
!target/release/bundle/msi/*.msi.zip
!target/release/bundle/msi/*.msi.zip.sig
!target/release/bundle/nsis/*.exe
!target/release/bundle/nsis/*.nsis.zip
!target/release/bundle/nsis/*.nsis.zip.sig
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-rust-target-
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm via corepack
shell: bash
run: |
corepack enable
corepack prepare --activate
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: install dependencies (ubuntu only)
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
- name: Install frontend dependencies
run: pnpm install
- name: build app (macos)
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config "tauri-release.conf.json"
if: startsWith(matrix.platform, 'macos')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: build app
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
id: build_os
if: "!startsWith(matrix.platform, 'macos')"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}
path: |
target/*/release/bundle/*/*.dmg
target/*/release/bundle/*/*.app.tar.gz
target/*/release/bundle/*/*.app.tar.gz.sig
target/release/bundle/*/*.dmg
target/release/bundle/*/*.app.tar.gz
target/release/bundle/*/*.app.tar.gz.sig
target/release/bundle/*/*.AppImage
target/release/bundle/*/*.AppImage.tar.gz
target/release/bundle/*/*.AppImage.tar.gz.sig
target/release/bundle/*/*.deb
target/release/bundle/*/*.rpm
target/release/bundle/msi/*.msi
target/release/bundle/msi/*.msi.zip
target/release/bundle/msi/*.msi.zip.sig
target/release/bundle/nsis/*.exe
target/release/bundle/nsis/*.nsis.zip
target/release/bundle/nsis/*.nsis.zip.sig

View File

@@ -1,80 +0,0 @@
name: CI
on:
push:
branches: ['main']
pull_request:
types: [opened, synchronize]
merge_group:
types: [checks_requested]
jobs:
build:
name: Build, Test, and Lint
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Cache turbo build setup
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Setup Node.JS environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm via corepack
shell: bash
run: |
corepack enable
corepack prepare --activate
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
env:
SQLX_OFFLINE: true
- name: Lint
run: pnpm lint
env:
SQLX_OFFLINE: true
- name: Start docker compose
run: docker compose up -d
- name: Test
run: pnpm test
env:
SQLX_OFFLINE: true
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres

163
.gitignore vendored
View File

@@ -1,63 +1,114 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
dist
tmp
/out-tsc
# dependencies
node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
/target
node_modules/
.svelte-kit/
theseus_gui/build/
theseus_gui/generated/
WixTools
.direnv/
.DS_Store
Thumbs.db
.pnpm-debug.log
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
minecraft
config
# frontend generated files
apps/frontend/src/generated
[#]*[#]
.turbo
target
generated
.env
# TEMPORARY: ignore my test instance and metadata
theseus_cli/foo
# app testing dir
app-playground-data/*
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# soley because i need the PORT to be 3002 due to WSL stuff
.env
apps/frontend/.env
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
theseus.iml
.astro

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

18
.idea/code.iml generated
View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/apps/daedalus_client/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/daedalus/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/apps/app-playground/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/apps/app/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/discord.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

View File

@@ -1,26 +0,0 @@
<component name="libraryTable">
<library name="KotlinJavaRuntime" type="repository">
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-sources.jar!/" />
</SOURCES>
</library>
</component>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2
.npmrc
View File

@@ -1,2 +0,0 @@
strict-peer-dependencies=false
auto-install-peers=true

View File

@@ -1,3 +1,12 @@
{
"recommendations": ["esbenp.prettier-vscode", "Vue.volar", "rust-lang.rust-analyzer"]
}
"recommendations": [
"tauri-apps.tauri-vscode",
"vunguyentuan.vscode-css-variables",
"stylelint.vscode-stylelint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"pivaszbs.svelte-autoimport",
"svelte.svelte-vscode",
"ardenivanov.svelte-intellisense"
]
}

160
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,160 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'theseus'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=theseus"
],
"filter": {
"name": "theseus",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'theseus_cli'",
"cargo": {
"args": [
"build",
"--bin=theseus_cli",
"--package=theseus_cli"
],
"filter": {
"name": "theseus_cli",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'theseus_cli'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=theseus_cli",
"--package=theseus_cli"
],
"filter": {
"name": "theseus_cli",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'theseus_playground'",
"cargo": {
"args": [
"build",
"--bin=theseus_playground",
"--package=theseus_playground"
],
"filter": {
"name": "theseus_playground",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'theseus_playground'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=theseus_playground",
"--package=theseus_playground"
],
"filter": {
"name": "theseus_playground",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'theseus_gui'",
"cargo": {
"args": [
"build",
"--bin=theseus_gui",
"--package=theseus_gui"
],
"filter": {
"name": "theseus_gui",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'theseus_gui'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=theseus_gui",
"--package=theseus_gui"
],
"filter": {
"name": "theseus_gui",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Tauri Development Debug",
"cargo": {
"args": [
"build",
"--manifest-path=./theseus_gui/src-tauri/Cargo.toml",
"--no-default-features"
]
},
"preLaunchTask": "ui:dev"
},
{
"type": "lldb",
"request": "launch",
"name": "Tauri Production Debug",
"cargo": {
"args": ["build", "--release", "--manifest-path=.theseus_gui/src-tauri/Cargo.toml"]
},
"preLaunchTask": "ui:build"
}
]
}

66
.vscode/settings.json vendored
View File

@@ -1,8 +1,60 @@
{
"prettier.endOfLine": "lf",
"editor.formatOnSave": true,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
"cssVariables.lookupFiles": [
"**/*.postcss",
"**/node_modules/omorphia/**/*.postcss"
],
"cssVariables.blacklistFolders": [
"**/.git",
"**/.svn",
"**/.hg",
"**/CVS",
"**/.DS_Store",
"**/.git",
"**/bower_components",
"**/tmp",
"**/dist",
"**/tests"
],
"gitlens.showWelcomeOnInstall": false,
"gitlens.showWhatsNewAfterUpgrades": false,
"gitlens.plusFeatures.enabled": false,
"gitlens.currentLine.enabled": false,
"gitlens.currentLine.pullRequests.enabled": false,
"gitlens.currentLine.scrollable": true,
"gitlens.codeLens.enabled": false,
"gitlens.hovers.enabled": false,
"CSSNavigation.activeCSSFileExtensions": [
"css",
"postcss"
],
"CSSNavigation.activeHTMLFileExtensions": [
"html",
"svelte",
"js",
"ts"
],
"CSSNavigation.excludeGlobPatterns": [
"**/bower_components/**",
"**/vendor/**",
"**/coverage/**"
],
"CSSNavigation.alwaysIncludeGlobPatterns": [
"./theseus_gui/node_modules/omorphia/**/*.postcss"
],
"html-css-class-completion.HTMLLanguages": [
"html",
"svelte"
],
"html-css-class-completion.includeGlobPattern": "**/*.{postcss,svelte}",
"html-css-class-completion.CSSLanguages": [
"postcss",
],
"svelte.enable-ts-plugin": true,
"svelte.ask-to-enable-ts-plugin": false,
"svelte.plugin.css.diagnostics.enable": false,
"svelte.plugin.svelte.diagnostics.enable": false,
"rust-analyzer.linkedProjects": [
"./theseus/Cargo.toml"
],
"rust-analyzer.showUnlinkedFileNotification": false,
}

32
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,32 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "ui:dev",
"type": "shell",
// `dev` keeps running in the background
// ideally you should also configure a `problemMatcher`
// see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson
"isBackground": true,
// change this to your `beforeDevCommand`:
"command": "yarn",
"args": ["dev"],
"options": {
"cwd": "${workspaceFolder}/theseus_gui"
}
},
{
"label": "ui:build",
"type": "shell",
// change this to your `beforeBuildCommand`:
"command": "yarn",
"args": ["build"],
"options": {
"cwd": "${workspaceFolder}/theseus_gui"
}
}
]
}

View File

@@ -1,13 +1,13 @@
# Copying Guidelines
# Copying
All packages in this repository are licensed under their respective licenses. For more information, refer to the LICENSE file in each package.
The source code of the theseus repository is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
For detailed information, consult each package's COPYING.md file, if available.
## Modrinth logo
## Modrinth Branding
Any files depicting the Modrinth branding, including the wrench-in-labyrinth logo, the landing image, and variations thereof, are licensed as follows:
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
> All rights reserved. © 2020-2023 Rinth, Inc.
All rights reserved. © 2020-2024 Rinth, Inc.
This includes, but may not be limited to, the following files:
If you fork this repository, you must remove all Modrinth branding assets from your fork.
- theseus_gui/src-tauri/icons/*

8938
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,24 @@
[workspace]
resolver = '2'
members = [
'./packages/app-lib',
'./apps/app-playground',
'./apps/app',
'./apps/labrinth',
'./apps/daedalus_client',
'./packages/daedalus',
"theseus",
"theseus_cli",
"theseus_playground",
"theseus_gui/src-tauri",
"theseus_macros"
]
[profile.dev]
opt-level = 0
debug = true
debug-assertions = true
overflow-checks = true
lto = false
panic = 'unwind'
incremental = true
codegen-units = 256
rpath = false
# Optimize for speed and reduce size on release builds
[profile.release]
panic = "abort" # Strip expensive panic clean-up logic
@@ -16,9 +26,3 @@ codegen-units = 1 # Compile crates one after another so the compiler can optimiz
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros]
opt-level = 3
[patch.crates-io]
wry = { git = "https://github.com/modrinth/wry", rev = "51907c6" }

View File

@@ -1,39 +1,9 @@
# ![Modrinth Monorepo Cover](/.github/assets/monorepo_cover.png)
# theseus
A game launcher which can be used as a CLI, GUI, and a library for creating and playing modrinth projects
![Issues](https://img.shields.io/github/issues-raw/Modrinth/code?color=c78aff&label=issues&style=for-the-badge)
![Pull Requests](https://img.shields.io/github/issues-pr-raw/Modrinth/code?color=c78aff&label=PRs&style=for-the-badge)
![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=c78aff&label=contributors&style=for-the-badge)
![Lines](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/modrinth/code/badge?style=flat&logoColor=white&color=c78aff&style=for-the-badge)
![Commit Activity](https://img.shields.io/github/commit-activity/m/Modrinth/code?color=c78aff&label=commits&style=for-the-badge)
![Last Commit](https://img.shields.io/github/last-commit/Modrinth/code?color=c78aff&label=last%20commit&style=for-the-badge)
Theseus aims to provide three components:
- A library (theseus)
- A CLI (theseus-cli)
- A GUI (theseus-gui)
## Modrinth Monorepo
Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains ![Lines](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/modrinth/olympus/badge?logoColor=white&color=black&label=) lines of code and has ![Contributors](https://img.shields.io/github/contributors/Modrinth/code?color=black&label=) contributors!
If you're not a developer and you've stumbled upon this repository, you can access the web interface on the [Modrinth website](https://modrinth.com) and download the latest release of the app [here](https://modrinth.com/app).
## Development
This repository contains two primary packages. For detailed development information, please refer to their respective READMEs:
- [Web Interface](apps/frontend/README.md)
- [Desktop App](apps/app/README.md)
## Contributing
We welcome contributions! Before submitting any contributions, please read our [contributing guidelines](https://docs.modrinth.com/contributing/getting-started/).
If you plan to fork this repository for your own purposes, please review our [copying guidelines](COPYING.md).
## Security
If you discover a security vulnerability within our codebase, please follow our [responsible disclosure guidelines](https://modrinth.com/legal/security).
## Support
If you need help with the Modrinth web interface or app, please visit our [support page](https://support.modrinth.com). For general inquiries, you can also join our [Discord server](https://discord.modrinth.com).
## License
All packages in this repository are licensed under their respective licenses. Refer to the LICENSE file in each package for more information.
Feel free to contribute!

View File

@@ -1 +0,0 @@
**/dist

View File

@@ -1,13 +0,0 @@
# Copying
The source code of the theseus repository is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
## Modrinth logo
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
> All rights reserved. © 2020-2023 Rinth, Inc.
This includes, but may not be limited to, the following files:
- theseus_gui/src-tauri/icons/\*

View File

@@ -1,22 +0,0 @@
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
import { fixupPluginRules } from '@eslint/compat'
import turboPlugin from 'eslint-plugin-turbo'
export default createConfigForNuxt().append([
{
name: 'turbo',
plugins: {
turbo: fixupPluginRules(turboPlugin),
},
rules: {
'turbo/no-undeclared-env-vars': 'error',
},
},
{
name: 'modrinth',
rules: {
'vue/html-self-closing': 'off',
'vue/multi-word-component-names': 'off',
},
},
])

View File

@@ -1,57 +0,0 @@
{
"name": "@modrinth/app-frontend",
"private": true,
"version": "0.9.3",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"tsc:check": "vue-tsc --noEmit",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
},
"dependencies": {
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.1",
"@tauri-apps/plugin-updater": "^2.3.0",
"@tauri-apps/plugin-window-state": "^2.2.0",
"@vintl/vintl": "^4.4.1",
"dayjs": "^1.11.10",
"floating-vue": "^5.2.2",
"ofetch": "^1.3.4",
"pinia": "^2.1.7",
"posthog-js": "^1.158.2",
"vite-svg-loader": "^5.1.0",
"vue": "^3.5.13",
"vue-multiselect": "3.0.0",
"vue-router": "4.3.0",
"vue-virtual-scroller": "v2.0.0-beta.8"
},
"devDependencies": {
"@eslint/compat": "^1.1.1",
"@formatjs/cli": "^6.2.12",
"@nuxt/eslint-config": "^0.5.6",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19",
"eslint": "^9.9.1",
"eslint-config-custom": "workspace:*",
"eslint-plugin-turbo": "^2.1.1",
"postcss": "^8.4.39",
"prettier": "^3.2.5",
"sass": "^1.74.1",
"tailwindcss": "^3.4.4",
"tsconfig": "workspace:*",
"typescript": "^5.5.4",
"vite": "^5.4.6",
"vue-tsc": "^2.1.6"
},
"packageManager": "pnpm@9.4.0",
"web-types": "../../web-types.json"
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,826 +0,0 @@
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import {
ArrowBigUpDashIcon,
CompassIcon,
DownloadIcon,
HomeIcon,
LeftArrowIcon,
LibraryIcon,
LogInIcon,
LogOutIcon,
MaximizeIcon,
MinimizeIcon,
PlusIcon,
RestoreIcon,
RightArrowIcon,
SettingsIcon,
XIcon,
} from '@modrinth/assets'
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
import { useLoading, useTheming } from '@/store/state'
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
import AccountsCard from '@/components/ui/AccountsCard.vue'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { get } from '@/helpers/settings'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { type } from '@tauri-apps/plugin-os'
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { getVersion } from '@tauri-apps/api/app'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { create_profile_and_install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import { useInstall } from '@/store/install.js'
import { invoke } from '@tauri-apps/api/core'
import { get_opening_command, initialize_state } from '@/helpers/state'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { renderString } from '@modrinth/utils'
import { useFetch } from '@/helpers/fetch.js'
import { check } from '@tauri-apps/plugin-updater'
import NavButton from '@/components/ui/NavButton.vue'
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
import { get_user } from '@/helpers/cache.js'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
import dayjs from 'dayjs'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
const themeStore = useTheming()
const news = ref([])
const urlModal = ref(null)
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const showOnboarding = ref(false)
const nativeDecorations = ref(false)
const os = ref('')
const stateInitialized = ref(false)
const criticalErrorMessage = ref()
const isMaximized = ref(false)
onMounted(async () => {
await useCheckDisableMouseover()
document.querySelector('body').addEventListener('click', handleClick)
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
})
onUnmounted(() => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
})
async function setupApp() {
stateInitialized.value = true
const {
native_decorations,
theme,
telemetry,
collapsed_navigation,
advanced_rendering,
onboarded,
default_page,
toggle_sidebar,
developer_mode,
feature_flags,
} = await get()
if (default_page === 'Library') {
await router.push('/library')
}
os.value = await getOS()
const dev = await isDev()
const version = await getVersion()
showOnboarding.value = !onboarded
nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations)
themeStore.setThemeState(theme)
themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering
themeStore.toggleSidebar = toggle_sidebar
themeStore.devMode = developer_mode
themeStore.featureFlags = feature_flags
isMaximized.value = await getCurrentWindow().isMaximized()
await getCurrentWindow().onResized(async () => {
isMaximized.value = await getCurrentWindow().isMaximized()
})
initAnalytics()
if (!telemetry) {
optOutAnalytics()
}
if (dev) debugAnalytics()
trackEvent('Launched', { version, dev, onboarded })
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
const osType = await type()
if (osType === 'macos') {
document.getElementsByTagName('html')[0].classList.add('mac')
} else {
document.getElementsByTagName('html')[0].classList.add('windows')
}
await warning_listener((e) =>
notificationsWrapper.value.addNotification({
title: 'Warning',
text: e.message,
type: 'warn',
}),
)
useFetch(
`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
'criticalAnnouncements',
true,
).then((res) => {
if (res && res.header && res.body) {
criticalErrorMessage.value = res
}
})
useFetch(`https://modrinth.com/blog/news.json`, 'news', true).then((res) => {
if (res && res.articles) {
news.value = res.articles
}
})
get_opening_command().then(handleCommand)
checkUpdates()
fetchCredentials()
}
const stateFailed = ref(false)
initialize_state()
.then(() => {
setupApp().catch((err) => {
stateFailed.value = true
console.error(err)
error.showError(err, null, false, 'state_init')
})
})
.catch((err) => {
stateFailed.value = true
console.error('Failed to initialize app', err)
error.showError(err, null, false, 'state_init')
})
const handleClose = async () => {
await saveWindowState(StateFlags.ALL)
await getCurrentWindow().close()
}
const router = useRouter()
router.afterEach((to, from, failure) => {
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
})
const route = useRoute()
const loading = useLoading()
loading.setEnabled(false)
const notifications = useNotifications()
const notificationsWrapper = ref()
const error = useError()
const errorModal = ref()
const install = useInstall()
const modInstallModal = ref()
const installConfirmModal = ref()
const incompatibilityWarningModal = ref()
const credentials = ref()
async function fetchCredentials() {
const creds = await getCreds().catch(handleError)
if (creds && creds.user_id) {
creds.user = await get_user(creds.user_id).catch(handleError)
}
credentials.value = creds
}
async function signIn() {
await login().catch(handleError)
await fetchCredentials()
}
async function logOut() {
await logout().catch(handleError)
await fetchCredentials()
}
const MIDAS_BITFLAG = 1 << 0
const hasPlus = computed(
() =>
credentials.value &&
credentials.value.user &&
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
)
const sidebarToggled = ref(true)
themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const showAd = computed(() => !(!sidebarVisible.value || hasPlus.value))
watch(
showAd,
() => {
if (!showAd.value) {
hide_ads_window(true)
} else {
init_ads_window(true)
}
},
{ immediate: true },
)
onMounted(() => {
invoke('show_window')
notifications.setNotifs(notificationsWrapper.value)
error.setErrorModal(errorModal.value)
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
install.setInstallConfirmModal(installConfirmModal)
install.setModInstallModal(modInstallModal)
})
const accounts = ref(null)
command_listener(handleCommand)
async function handleCommand(e) {
if (!e) return
if (e.event === 'RunMRPack') {
// RunMRPack should directly install a local mrpack given a path
if (e.path.endsWith('.mrpack')) {
await create_profile_and_install_from_file(e.path).catch(handleError)
trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop',
})
}
} else {
// Other commands are URL-based (deep linking)
urlModal.value.show(e)
}
}
const updateAvailable = ref(false)
async function checkUpdates() {
const update = await check()
updateAvailable.value = !!update
setTimeout(
() => {
checkUpdates()
},
5 * 1000 * 60,
)
}
function handleClick(e) {
let target = e.target
while (target != null) {
if (target.matches('a')) {
if (
target.href &&
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
!target.classList.contains('router-link-active') &&
!target.href.startsWith('http://localhost') &&
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
) {
openUrl(target.href)
}
e.preventDefault()
break
}
target = target.parentElement
}
}
function handleAuxClick(e) {
// disables middle click -> new tab
if (e.button === 1) {
e.preventDefault()
// instead do a left click
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
})
e.target.dispatchEvent(event)
}
}
</script>
<template>
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
<div id="teleports"></div>
<div v-if="stateInitialized" class="app-grid-layout relative">
<Suspense>
<AppSettingsModal ref="settingsModal" />
</Suspense>
<Suspense>
<InstanceCreationModal ref="installationModal" />
</Suspense>
<div
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
>
<NavButton v-tooltip.right="'Home'" to="/">
<HomeIcon />
</NavButton>
<NavButton
v-tooltip.right="'Discover content'"
to="/browse/modpack"
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
>
<CompassIcon />
</NavButton>
<NavButton
v-tooltip.right="'Library'"
to="/library"
:is-subpage="
() =>
route.path.startsWith('/instance') ||
((route.path.startsWith('/browse') || route.path.startsWith('/project')) &&
route.query.i)
"
>
<LibraryIcon />
</NavButton>
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
<suspense>
<QuickInstanceSwitcher />
</suspense>
<NavButton
v-tooltip.right="'Create new instance'"
:to="() => $refs.installationModal.show()"
:disabled="offline"
>
<PlusIcon />
</NavButton>
<div class="flex flex-grow"></div>
<NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
<DownloadIcon />
</NavButton>
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<SettingsIcon />
</NavButton>
<ButtonStyled v-if="credentials" type="transparent" circular>
<OverflowMenu
:options="[
{
id: 'sign-out',
action: () => logOut(),
color: 'danger',
},
]"
direction="left"
>
<Avatar
:src="credentials.user.avatar_url"
:alt="credentials.user.username"
size="32px"
circle
/>
<template #sign-out> <LogOutIcon /> Sign out </template>
</OverflowMenu>
</ButtonStyled>
<NavButton v-else v-tooltip.right="'Sign in'" :to="() => signIn()">
<LogInIcon />
<template #label>Sign in</template>
</NavButton>
</div>
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
<div data-tauri-drag-region class="flex p-3">
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
<div class="flex items-center gap-1 ml-3">
<button
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
@click="router.back()"
>
<LeftArrowIcon />
</button>
<button
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
@click="router.forward()"
>
<RightArrowIcon />
</button>
</div>
<Breadcrumbs class="pt-[2px]" />
</div>
<section class="flex ml-auto items-center">
<ButtonStyled
v-if="!forceSidebar && themeStore.toggleSidebar"
:type="sidebarToggled ? 'standard' : 'transparent'"
circular
>
<button
class="mr-3 transition-transform"
:class="{ 'rotate-180': !sidebarToggled }"
@click="sidebarToggled = !sidebarToggled"
>
<RightArrowIcon />
</button>
</ButtonStyled>
<div class="flex mr-3">
<Suspense>
<RunningAppBar />
</Suspense>
</div>
<section v-if="!nativeDecorations" class="window-controls">
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
<MinimizeIcon />
</Button>
<Button
class="titlebar-button"
icon-only
@click="() => getCurrentWindow().toggleMaximize()"
>
<RestoreIcon v-if="isMaximized" />
<MaximizeIcon v-else />
</Button>
<Button class="titlebar-button close" icon-only @click="handleClose">
<XIcon />
</Button>
</section>
</section>
</div>
</div>
<div
v-if="stateInitialized"
class="app-contents experimental-styles-within"
:class="{ 'sidebar-enabled': sidebarVisible }"
>
<div class="app-viewport flex-grow router-view">
<div
class="loading-indicator-container h-8 fixed z-50"
:style="{
top: 'calc(var(--top-bar-height))',
left: 'calc(var(--left-bar-width))',
width: 'calc(100% - var(--left-bar-width) - var(--right-bar-width))',
}"
>
<ModrinthLoadingIndicator />
</div>
<div
v-if="themeStore.featureFlags.page_path"
class="absolute bottom-0 left-0 m-2 bg-tooltip-bg text-tooltip-text font-semibold rounded-full px-2 py-1 text-xs z-50"
>
{{ route.fullPath }}
</div>
<div
id="background-teleport-target"
class="absolute h-full -z-10 rounded-tl-[--radius-xl] overflow-hidden"
:style="{
width: 'calc(100% - var(--right-bar-width))',
}"
></div>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
<component :is="Component"></component>
</Suspense>
</template>
</RouterView>
</div>
<div
class="app-sidebar mt-px shrink-0 flex flex-col border-0 border-l-[1px] border-[--brand-gradient-border] border-solid overflow-auto"
:class="{ 'has-plus': hasPlus }"
>
<div
class="app-sidebar-scrollable flex-grow shrink overflow-y-auto relative"
:class="{ 'pb-12': !hasPlus }"
>
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
<div class="sidebar-default-content" :class="{ 'sidebar-enabled': sidebarVisible }">
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
<h3 class="text-lg m-0">Playing as</h3>
<suspense>
<AccountsCard ref="accounts" mode="small" />
</suspense>
</div>
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
<suspense>
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
</suspense>
</div>
<div v-if="news && news.length > 0" class="pt-4 flex flex-col">
<h3 class="px-4 text-lg m-0">News</h3>
<template v-for="(item, index) in news" :key="`news-${index}`">
<a
:class="`flex flex-col outline-offset-[-4px] hover:bg-[--brand-gradient-border] focus:bg-[--brand-gradient-border] px-4 transition-colors ${index === 0 ? 'pt-2 pb-4' : 'py-4'}`"
:href="item.link"
target="_blank"
rel="external"
>
<img
:src="item.thumbnail"
alt="News thumbnail"
aria-hidden="true"
class="w-full aspect-[3/1] object-cover rounded-2xl border-[1px] border-solid border-[--brand-gradient-border]"
/>
<h4 class="mt-2 mb-0 text-sm leading-none text-contrast font-semibold">
{{ item.title }}
</h4>
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
{{ dayjs(item.date).fromNow() }}
</p>
</a>
<hr
v-if="index !== news.length - 1"
class="h-px my-[-2px] mx-4 border-0 m-0 bg-[--brand-gradient-border]"
/>
</template>
</div>
</div>
</div>
<template v-if="showAd">
<a
href="https://modrinth.plus?app"
class="absolute bottom-[250px] w-full flex justify-center items-center gap-1 px-4 py-3 text-purple font-medium hover:underline z-10"
target="_blank"
>
<ArrowBigUpDashIcon class="text-2xl" /> Upgrade to Modrinth+
</a>
<PromotionWrapper />
</template>
</div>
<div class="view">
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
<h1>{{ criticalErrorMessage.header }}</h1>
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
</div>
</div>
</div>
<URLConfirmModal ref="urlModal" />
<Notifications ref="notificationsWrapper" sidebar />
<ErrorModal ref="errorModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<InstallConfirmModal ref="installConfirmModal" />
</template>
<style lang="scss" scoped>
.window-controls {
z-index: 20;
display: none;
flex-direction: row;
align-items: center;
.titlebar-button {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all ease-in-out 0.1s;
background-color: transparent;
color: var(--color-base);
height: 100%;
width: 3rem;
position: relative;
box-shadow: none;
&:last-child {
padding-right: 0.75rem;
width: 3.75rem;
}
svg {
width: 1.25rem;
height: 1.25rem;
}
&::before {
content: '';
border-radius: 999999px;
width: 3rem;
height: 3rem;
aspect-ratio: 1 / 1;
margin-block: auto;
position: absolute;
background-color: transparent;
scale: 0.9;
transition: all ease-in-out 0.2s;
z-index: -1;
}
&.close {
&:hover,
&:active {
color: var(--color-accent-contrast);
&::before {
background-color: var(--color-red);
}
}
}
&:hover,
&:active {
color: var(--color-contrast);
&::before {
background-color: var(--color-button-bg);
scale: 1;
}
}
}
}
.app-grid-layout,
.app-contents {
--top-bar-height: 3rem;
--left-bar-width: 4rem;
--right-bar-width: 300px;
}
.app-grid-layout {
display: grid;
grid-template: 'status status' 'nav dummy';
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
position: relative;
//z-index: 0;
background-color: var(--color-raised-bg);
height: 100vh;
}
.app-grid-navbar {
grid-area: nav;
}
.app-grid-statusbar {
grid-area: status;
}
.app-contents {
position: absolute;
z-index: 1;
left: var(--left-bar-width);
top: var(--top-bar-height);
right: 0;
bottom: 0;
height: calc(100vh - var(--top-bar-height));
background-color: var(--color-bg);
border-top-left-radius: var(--radius-xl);
display: grid;
grid-template-columns: 1fr 0px;
// transition: grid-template-columns 0.4s ease-in-out;
&.sidebar-enabled {
grid-template-columns: 1fr 300px;
}
}
.loading-indicator-container {
border-top-left-radius: var(--radius-xl);
overflow: hidden;
}
.app-sidebar {
overflow: visible;
width: 300px;
position: relative;
height: calc(100vh - var(--top-bar-height));
background: var(--brand-gradient-bg);
--color-button-bg: var(--brand-gradient-button);
--color-button-bg-hover: var(--brand-gradient-border);
--color-divider: var(--brand-gradient-border);
--color-divider-dark: var(--brand-gradient-border);
}
.app-sidebar::after {
content: '';
position: absolute;
bottom: 250px;
left: 0;
right: 0;
height: 5rem;
background: var(--brand-gradient-fade-out-color);
pointer-events: none;
}
.app-sidebar.has-plus::after {
display: none;
}
.app-sidebar::before {
content: '';
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.2) inset;
top: 0;
bottom: 0;
left: -2rem;
width: 2rem;
position: absolute;
pointer-events: none;
}
.app-viewport {
flex-grow: 1;
height: 100%;
overflow: auto;
overflow-x: hidden;
}
.app-contents::before {
z-index: 1;
content: '';
position: fixed;
left: var(--left-bar-width);
top: var(--top-bar-height);
right: calc(-1 * var(--left-bar-width));
bottom: calc(-1 * var(--left-bar-width));
border-radius: var(--radius-xl);
box-shadow:
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
pointer-events: none;
}
.sidebar-teleport-content {
display: contents;
}
.sidebar-default-content {
display: none;
}
.sidebar-teleport-content:empty + .sidebar-default-content.sidebar-enabled {
display: contents;
}
</style>
<style>
.mac {
.app-grid-statusbar {
padding-left: 5rem;
}
}
.windows {
.fake-appbar {
height: 2.5rem !important;
}
.window-controls {
display: flex !important;
}
.info-card {
right: 8rem;
}
.profile-card {
right: 8rem;
}
}
</style>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" version="1.1" viewBox="0 0 1793 199">
<g>
<g id="Layer_1">
<g id="green" fill="var(--color-brand)">
<path d="M1184.1,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
<path d="M1291.1,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
<path d="M1357.2,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
<path d="M1460,165.3l-40.8-95.1h23.2l35.1,83.9h-11.4l36.3-83.9h21.4l-40.8,95.1h-23Z"/>
<path d="M1579.6,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
<path d="M1645.7,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
<path d="M1749.9,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
<g>
<path d="M9.8,143l63.4-38.1-5.8-15.3,18.1-18.6,22.9-4.9,6.6,8.2-10.6,10.7-9.2,2.9-6.6,6.8,3.2,9,6.5,6.9,9.2-2.5,6.6-7.2,14.3-4.5,4.3,9.6-14.8,18.1-24.8,7.8-11.1-12.4-63.6,38.2c-3-3.9-6.5-9.4-8.8-14.7ZM192.8,65.4l-50.4,13.6c2.8,7.4,3.7,11.7,4.5,16.5l50.3-13.6c-.8-5.4-2.2-10.8-4.4-16.5Z" fill-rule="evenodd"/>
<path d="M17.3,106.5c3.6,42.1,38.9,75.2,82,75.2s60.7-18.9,74-46.3l16.4,5.7c-15.8,34.1-50.3,57.9-90.4,57.9S3.6,158.2,0,106.5h17.3ZM.3,89.4C5.3,39.2,47.8,0,99.3,0s99.5,44.6,99.5,99.5-1.1,17.4-3.3,25.5l-16.3-5.7c1.6-6.5,2.4-13.1,2.4-19.8,0-45.4-36.9-82.3-82.3-82.3S22.6,48.7,17.6,89.4H.3Z" fill-rule="evenodd"/>
<path d="M99,51.6c-26.4,0-47.9,21.5-47.9,48s21.5,48,48,48,2.7,0,4-.2l4.8,16.8c-2.9.4-5.8.6-8.8.6-36,0-65.2-29.2-65.2-65.2S63.1,34.4,99,34.4s1.8,0,2.7,0l-2.7,17.1ZM118.6,37.4c26.4,8.3,45.6,33,45.6,62.2s-16.4,50.2-39.8,60l-4.8-16.7c16.2-7.7,27.4-24.2,27.4-43.3s-13-38.1-31.1-44.9l2.7-17.2Z" fill-rule="evenodd"/>
</g>
</g>
<g id="black" fill="currentColor">
<path d="M354.8,69.2c12,0,21.7,3.4,28.6,10.4,7,7.2,10.6,17.5,10.6,31.5v54.8h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,5-6.8,12.2-6.8,21.3v48.5h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,4.8-6.8,12-6.8,21.3v48.5h-22.4v-95.6h21.3v12.2c3.6-4.3,8.1-7.5,13.4-9.8,5.4-2.3,11.3-3.4,17.9-3.4s13.6,1.3,19.2,3.9c5.5,2.9,9.8,6.8,13.1,12,3.9-5,8.9-8.9,15.2-11.8,6.3-2.7,13.1-4.1,20.6-4.1ZM466,167.2c-9.7,0-18.4-2.1-26.1-6.3-7.6-4-13.8-10.1-18.1-17.5-4.5-7.3-6.6-15.7-6.6-25.2s2.1-17.9,6.6-25.2c4.3-7.4,10.6-13.4,18.1-17.4,7.7-4.1,16.5-6.3,26.1-6.3s18.6,2.1,26.3,6.3c7.7,4.1,13.8,10,18.3,17.4,4.3,7.3,6.4,15.7,6.4,25.2s-2.1,17.9-6.4,25.2c-4.5,7.5-10.6,13.4-18.3,17.5-7.7,4.1-16.5,6.3-26.3,6.3h0ZM466,148c8.2,0,15-2.7,20.4-8.2,5.4-5.5,8.1-12.7,8.1-21.7s-2.7-16.1-8.1-21.7c-5.4-5.5-12.2-8.2-20.4-8.2s-15,2.7-20.2,8.2c-5.4,5.5-8.1,12.7-8.1,21.7s2.7,16.1,8.1,21.7c5.2,5.5,12,8.2,20.2,8.2ZM631.5,33.1v132.8h-21.5v-12.3c-3.7,4.4-8.3,7.9-13.6,10.2-5.5,2.3-11.5,3.4-18.1,3.4s-17.4-2-24.7-6.1c-7.3-4.1-13.2-9.8-17.4-17.4-4.1-7.3-6.3-15.9-6.3-25.6s2.1-18.3,6.3-25.6c4.1-7.3,10-13.1,17.4-17.2,7.3-4.1,15.6-6.1,24.7-6.1s12.2,1.1,17.4,3.2c5.2,2.1,9.8,5.4,13.4,9.7v-49h22.4ZM581.1,148c5.4,0,10.2-1.3,14.5-3.8,4.3-2.3,7.7-5.9,10.2-10.4,2.5-4.5,3.8-9.8,3.8-15.7s-1.3-11.3-3.8-15.7c-2.5-4.5-5.9-8.1-10.2-10.6-4.3-2.3-9.1-3.6-14.5-3.6s-10.2,1.3-14.5,3.6c-4.3,2.5-7.7,6.1-10.2,10.6-2.5,4.5-3.8,9.8-3.8,15.7s1.3,11.3,3.8,15.7c2.5,4.5,5.9,8.1,10.2,10.4,4.3,2.5,9.1,3.8,14.5,3.8ZM681.6,84.3c6.4-10,17.7-15,34-15v21.3c-1.7-.3-3.4-.5-5.2-.5-8.8,0-15.6,2.5-20.4,7.5-4.8,5.2-7.3,12.5-7.3,22v46.4h-22.4v-95.6h21.3v14h0ZM734.1,70.3h22.4v95.6h-22.4v-95.6ZM745.4,54.6c-4.1,0-7.5-1.3-10.2-3.9-2.7-2.4-4.2-5.9-4.1-9.5,0-3.8,1.4-7,4.1-9.7,2.7-2.5,6.1-3.8,10.2-3.8s7.5,1.3,10.2,3.6c2.7,2.5,4.1,5.5,4.1,9.3s-1.3,7.2-3.9,9.8c-2.7,2.7-6.3,4.1-10.4,4.1ZM839.5,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4v-95.6h21.3v12.3c3.8-4.5,8.4-7.7,14-10,5.5-2.3,12-3.4,19-3.4ZM964.8,160.7c-2.8,2.2-6,3.9-9.5,4.8-3.9,1.1-7.9,1.6-12,1.6-10.6,0-18.6-2.7-24.3-8.2-5.7-5.5-8.6-13.4-8.6-24v-46h-15.7v-17.9h15.7v-21.8h22.4v21.8h25.6v17.9h-25.6v45.5c0,4.7,1.1,8.2,3.4,10.6,2.3,2.5,5.5,3.8,9.8,3.8s9.1-1.3,12.5-3.9l6.3,15.9ZM1036.9,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4V33.1h22.4v48.3c3.8-3.9,8.2-7,13.8-9.1,5.4-2,11.5-3,18.1-3Z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -1,141 +0,0 @@
<script setup>
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { useLoading } from '@/store/state.js'
const props = defineProps({
throttle: {
type: Number,
default: 0,
},
duration: {
type: Number,
default: 1000,
},
height: {
type: Number,
default: 2,
},
color: {
type: String,
default: 'var(--loading-bar-gradient)',
},
})
const indicator = useLoadingIndicator({
duration: props.duration,
throttle: props.throttle,
})
onBeforeUnmount(() => indicator.clear)
const loading = useLoading()
watch(loading, (newValue) => {
if (newValue.barEnabled) {
if (newValue.loading) {
indicator.start()
} else {
indicator.finish()
}
}
})
function useLoadingIndicator(opts) {
const progress = ref(0)
const isLoading = ref(false)
const step = computed(() => 10000 / opts.duration)
let _timer = null
let _throttle = null
function start() {
clear()
progress.value = 0
if (opts.throttle) {
_throttle = setTimeout(() => {
isLoading.value = true
_startTimer()
}, opts.throttle)
} else {
isLoading.value = true
_startTimer()
}
}
function finish() {
progress.value = 100
_hide()
}
function clear() {
clearInterval(_timer)
clearTimeout(_throttle)
_timer = null
_throttle = null
}
function _increase(num) {
progress.value = Math.min(100, progress.value + num)
}
function _hide() {
clear()
setTimeout(() => {
isLoading.value = false
setTimeout(() => {
progress.value = 0
}, 400)
}, 500)
}
function _startTimer() {
_timer = setInterval(() => {
_increase(step.value)
}, 100)
}
return { progress, isLoading, start, finish, clear }
}
</script>
<template>
<div
class="loading-indicator-bar"
:style="{
'--_width': `${indicator.progress.value}%`,
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
top: `0`,
right: `0`,
left: `${props.offsetWidth}`,
pointerEvents: 'none',
width: `var(--_width)`,
height: `var(--_height)`,
borderRadius: `var(--_height)`,
// opacity: `var(--_opacity)`,
background: `${props.color}`,
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
zIndex: 6,
}"
>
<slot />
</div>
</template>
<style lang="scss" scoped>
.loading-indicator-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
width: var(--_width);
bottom: 0;
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
opacity: calc(var(--_opacity) * 0.1);
z-index: 5;
transition:
width 0.1s ease-in-out,
opacity 0.1s ease-out;
}
</style>

View File

@@ -1,60 +0,0 @@
<script setup lang="ts">
import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { add_project_from_path } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router'
const props = defineProps({
instance: {
type: Object,
required: true,
},
})
const router = useRouter()
const handleAddContentFromFile = async () => {
const newProject = await open({ multiple: true })
if (!newProject) return
for (const project of newProject) {
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
}
}
const handleSearchContent = async () => {
await router.push({
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
query: { i: props.instance.path },
})
}
</script>
<template>
<div class="joined-buttons">
<ButtonStyled>
<button @click="handleSearchContent">
<PlusIcon />
Install content
</button>
</ButtonStyled>
<ButtonStyled>
<OverflowMenu
:options="[
{
id: 'from_file',
action: handleAddContentFromFile,
},
]"
>
<DropdownIcon />
<template #from_file>
<FolderOpenIcon />
<span class="no-wrap"> Add from file </span>
</template>
</OverflowMenu>
</ButtonStyled>
</div>
</template>

View File

@@ -1,62 +0,0 @@
<template>
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
<ChevronLeftIcon />
</Button>
<Button
v-if="false"
class="breadcrumbs__forward transparent"
icon-only
@click="$router.forward()"
>
<ChevronRightIcon />
</Button>
{{ breadcrumbData.resetToNames(breadcrumbs) }}
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
<router-link
v-if="breadcrumb.link"
:to="{
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
query: breadcrumb.query,
}"
>{{
breadcrumb.name.charAt(0) === '?'
? breadcrumbData.getName(breadcrumb.name.slice(1))
: breadcrumb.name
}}
</router-link>
<span
v-else
data-tauri-drag-region
class="text-contrast font-semibold cursor-default select-none"
>{{
breadcrumb.name.charAt(0) === '?'
? breadcrumbData.getName(breadcrumb.name.slice(1))
: breadcrumb.name
}}</span
>
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
</template>
</div>
</template>
<script setup>
import { ChevronRightIcon, ChevronLeftIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useRoute } from 'vue-router'
import { computed } from 'vue'
const route = useRoute()
const breadcrumbData = useBreadcrumbs()
const breadcrumbs = computed(() => {
const additionalContext =
route.meta.useContext === true
? breadcrumbData.context
: route.meta.useRootContext === true
? breadcrumbData.rootContext
: null
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
})
</script>

View File

@@ -1,331 +0,0 @@
<script setup>
import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets'
import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleError } from '@/store/notifications.js'
import { handleSevereError } from '@/store/error.js'
import { cancel_directory_change } from '@/helpers/settings.js'
import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const errorModal = ref()
const error = ref()
const closable = ref(true)
const title = ref('An error occurred')
const errorType = ref('unknown')
const supportLink = ref('https://support.modrinth.com')
const metadata = ref({})
defineExpose({
async show(errorVal, context, canClose = true, source = null) {
closable.value = canClose
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
title.value = 'Unable to sign in to Minecraft'
errorType.value = 'minecraft_auth'
supportLink.value =
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
if (
errorVal.message.includes('existing connection was forcibly closed') ||
errorVal.message.includes('error sending request for url')
) {
metadata.value.network = true
}
if (errorVal.message.includes('because the target machine actively refused it')) {
metadata.value.hostsFile = true
}
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
title.value = 'Sign in to Minecraft'
errorType.value = 'minecraft_sign_in'
supportLink.value = 'https://support.modrinth.com'
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
title.value = 'Could not change app directory'
errorType.value = 'directory_move'
supportLink.value = 'https://support.modrinth.com'
if (errorVal.message.includes('directory is not writeable')) {
metadata.value.readOnly = true
}
if (errorVal.message.includes('Not enough space')) {
metadata.value.notEnoughSpace = true
}
} else if (errorVal.message && errorVal.message.includes('No loader version selected for')) {
title.value = 'No loader selected'
errorType.value = 'no_loader_version'
supportLink.value = 'https://support.modrinth.com'
metadata.value.profilePath = context.profilePath
} else if (source === 'state_init') {
title.value = 'Error initializing Modrinth App'
errorType.value = 'state_init'
supportLink.value = 'https://support.modrinth.com'
} else {
title.value = 'An error occurred'
errorType.value = 'unknown'
supportLink.value = 'https://support.modrinth.com'
metadata.value = {}
}
error.value = errorVal
errorModal.value.show()
},
})
const loadingMinecraft = ref(false)
async function loginMinecraft() {
try {
loadingMinecraft.value = true
const loggedIn = await login_flow()
if (loggedIn) {
await set_default_user(loggedIn.id).catch(handleError)
}
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
loadingMinecraft.value = false
errorModal.value.hide()
} catch (err) {
loadingMinecraft.value = false
handleSevereError(err)
}
}
async function cancelDirectoryChange() {
try {
await cancel_directory_change()
window.location.reload()
} catch (err) {
handleError(err)
}
}
function retryDirectoryChange() {
window.location.reload()
}
const loadingRepair = ref(false)
async function repairInstance() {
loadingRepair.value = true
try {
await install(metadata.value.profilePath, false)
errorModal.value.hide()
} catch (err) {
handleSevereError(err)
}
loadingRepair.value = false
}
</script>
<template>
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
<template v-if="metadata.network">
<h3>Network issues</h3>
<p>
It looks like there were issues with the Modrinth App connecting to Microsoft's
servers. This is often the result of a poor connection, so we recommend trying again
to see if it works. If issues continue to persist, follow the steps in
<a
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
>
our support article
</a>
to troubleshoot.
</p>
</template>
<template v-else-if="metadata.hostsFile">
<h3>Network issues</h3>
<p>
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
remote server rejected the connection. This may indicate that these services are
blocked by the hosts file. Please visit
<a
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
>
our support article
</a>
for steps on how to fix the issue.
</p>
</template>
<template v-else>
<h3>Try another Microsoft account</h3>
<p>
Double check you've signed in with the right account. You may own Minecraft on a
different Microsoft account.
</p>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Try another account
</button>
</div>
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
<p>
Try signing in with the
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
first. Once you're done, come back here and sign in!
</p>
</template>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Try signing in again
</button>
</div>
</template>
<template v-if="errorType === 'directory_move'">
<template v-if="metadata.readOnly">
<h3>Change directory permissions</h3>
<p>
It looks like the Modrinth App is unable to write to the directory you selected.
Please adjust the permissions of the directory and try again or cancel the directory
change.
</p>
</template>
<template v-else-if="metadata.notEnoughSpace">
<h3>Not enough space</h3>
<p>
It looks like there is not enough space on the disk containing the dirctory you
selected Please free up some space and try again or cancel the directory change.
</p>
</template>
<template v-else>
<p>
The Modrinth App is unable to migrate to the new directory you selected. Please
contact support for help or cancel the directory change.
</p>
</template>
<div class="cta-button">
<button class="btn" @click="retryDirectoryChange">
<UpdatedIcon /> Retry directory change
</button>
<button class="btn btn-danger" @click="cancelDirectoryChange">
<XIcon /> Cancel directory change
</button>
</div>
</template>
<div v-else-if="errorType === 'minecraft_sign_in'">
<p>
To play this instance, you must sign in through Microsoft below. If you don't have a
Minecraft account, you can purchase the game on the
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
>Minecraft website</a
>.
</p>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Sign in to Minecraft
</button>
</div>
</div>
<template v-else-if="errorType === 'state_init'">
<p>
Modrinth App failed to load correctly. This may be because of a corrupted file, or
because the app is missing crucial files.
</p>
<p>You may be able to fix it through one of the following ways:</p>
<ul>
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
<li>Redownloading the app.</li>
</ul>
</template>
<template v-else-if="errorType === 'no_loader_version'">
<p>The Modrinth App failed to find the loader version for this instance.</p>
<p>To resolve this, you need to repair the instance. Click the button below to do so.</p>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingRepair" @click="repairInstance">
<HammerIcon /> Repair instance
</button>
</div>
</template>
<template v-else>
{{ error.message ?? error }}
</template>
<template
v-if="
errorType === 'directory_move' ||
errorType === 'minecraft_auth' ||
errorType === 'state_init' ||
errorType === 'no_loader_version'
"
>
<hr />
<p>
If nothing is working and you need help, visit
<a :href="supportLink">our support page</a>
and start a chat using the widget in the bottom right and we will be more than happy to
assist! Make sure to provide the following debug information to the agent:
</p>
<details>
<summary>Debug information</summary>
{{ error.message ?? error }}
</details>
</template>
</div>
<div class="input-group push-right">
<a :href="supportLink" class="btn" @click="errorModal.hide()"><ChatIcon /> Get support</a>
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</ModalWrapper>
</template>
<style>
.light-mode {
--color-orange-bg: rgba(255, 163, 71, 0.2);
}
.dark-mode,
.oled-mode {
--color-orange-bg: rgba(224, 131, 37, 0.2);
}
</style>
<style scoped lang="scss">
.cta-button {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
gap: 0.5rem;
}
.warning-banner {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: var(--gap-lg);
background-color: var(--color-orange-bg);
border: 2px solid var(--color-orange);
border-radius: var(--radius-md);
margin-bottom: 1rem;
}
.warning-banner__title {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
svg {
color: var(--color-orange);
height: 1.5rem;
width: 1.5rem;
}
}
.modal-body {
display: flex;
flex-direction: column;
gap: var(--gap-md);
}
.markdown-body {
overflow: auto;
}
</style>

View File

@@ -1,246 +0,0 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import {
DownloadIcon,
GameIcon,
PlayIcon,
SpinnerIcon,
StopCircleIcon,
TimerIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { finish_install, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
import { process_listener } from '@/helpers/events'
import { handleError } from '@/store/state.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
import { trackEvent } from '@/helpers/analytics'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { formatCategory } from '@modrinth/utils'
dayjs.extend(relativeTime)
const props = defineProps({
instance: {
type: Object,
default() {
return {}
},
},
compact: {
type: Boolean,
default: false,
},
first: {
type: Boolean,
default: false,
},
})
const playing = ref(false)
const loading = ref(false)
const modLoading = computed(
() =>
loading.value ||
currentEvent.value === 'installing' ||
(currentEvent.value === 'launched' && !playing.value),
)
const installing = computed(() => props.instance.install_stage.includes('installing'))
const installed = computed(() => props.instance.install_stage === 'installed')
const router = useRouter()
const seeInstance = async () => {
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
}
const checkProcess = async () => {
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
playing.value = runningProcesses.length > 0
}
const play = async (e, context) => {
e?.stopPropagation()
loading.value = true
await run(props.instance.path)
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
.finally(() => {
trackEvent('InstancePlay', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
})
loading.value = false
}
const stop = async (e, context) => {
e?.stopPropagation()
playing.value = false
await kill(props.instance.path).catch(handleError)
trackEvent('InstanceStop', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
}
const repair = async (e) => {
e?.stopPropagation()
await finish_install(props.instance)
}
const openFolder = async () => {
await showProfileInFolder(props.instance.path)
}
const addContent = async () => {
await router.push({
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.path },
})
}
defineExpose({
play,
stop,
seeInstance,
openFolder,
addContent,
instance: props.instance,
})
const currentEvent = ref(null)
const unlisten = await process_listener((e) => {
if (e.profile_path_id === props.instance.path) {
currentEvent.value = e.event
if (e.event === 'finished') {
playing.value = false
}
}
})
onMounted(() => checkProcess())
onUnmounted(() => unlisten())
</script>
<template>
<template v-if="compact">
<div
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
@click="seeInstance"
@mouseenter="checkProcess"
>
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.path"
alt="Mod card"
/>
<div class="h-full flex items-center font-bold text-contrast leading-normal">
<span class="line-clamp-2">{{ instance.name }}</span>
</div>
<div class="flex items-center">
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
<StopCircleIcon />
</button>
</ButtonStyled>
<ButtonStyled v-else-if="modLoading" color="standard" circular>
<button v-tooltip="'Instance is loading...'" disabled>
<SpinnerIcon class="animate-spin" />
</button>
</ButtonStyled>
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
<button
v-tooltip="'Play'"
@click="(e) => play(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<!-- Translate for optical centering -->
<PlayIcon class="translate-x-[1px]" />
</button>
</ButtonStyled>
</div>
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
<TimerIcon />
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
</div>
</div>
</template>
<div v-else>
<div
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
@click="seeInstance"
@mouseenter="checkProcess"
>
<div class="relative flex items-center justify-center">
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.path"
alt="Mod card"
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
/>
<div class="absolute inset-0 flex items-center justify-center">
<ButtonStyled v-if="playing" size="large" color="red" circular>
<button
v-tooltip="'Stop'"
:class="{ 'scale-100 opacity-100': playing }"
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
@click="(e) => stop(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<StopCircleIcon />
</button>
</ButtonStyled>
<SpinnerIcon
v-else-if="modLoading || installing"
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
class="animate-spin w-8 h-8"
tabindex="-1"
/>
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
<button
v-tooltip="'Repair'"
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
@click="(e) => repair(e)"
>
<DownloadIcon />
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand" circular>
<button
v-tooltip="'Play'"
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
@click="(e) => play(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<PlayIcon class="translate-x-[2px]" />
</button>
</ButtonStyled>
</div>
</div>
<div class="flex flex-col gap-1">
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
{{ instance.name }}
</p>
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
<GameIcon class="shrink-0" />
<span class="text-sm">
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
</span>
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,53 +0,0 @@
<script setup lang="ts">
import { convertFileSrc } from '@tauri-apps/api/core'
import { formatCategory } from '@modrinth/utils'
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
type Instance = {
game_version: string
loader: string
path: string
install_stage: string
icon_path?: string
name: string
}
defineProps<{
instance: Instance
}>()
</script>
<template>
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
<router-link
:to="`/instance/${encodeURIComponent(instance.path)}`"
tabindex="-1"
class="flex flex-col gap-4 text-primary"
>
<span class="flex items-center gap-2">
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
:alt="instance.name"
size="48px"
/>
<span class="flex flex-col gap-2">
<span class="font-extrabold bold text-contrast">
{{ instance.name }}
</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 }}
</span>
</span>
</span>
</router-link>
<ButtonStyled>
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
<LeftArrowIcon /> Back to instance
</router-link>
</ButtonStyled>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,59 +0,0 @@
<template>
<RouterLink
v-if="typeof to === 'string'"
:to="to"
v-bind="$attrs"
:class="{
'router-link-active': isPrimary && isPrimary(route),
'subpage-active': isSubpage && isSubpage(route),
}"
class="w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
>
<slot />
</RouterLink>
<button
v-else
v-bind="$attrs"
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
@click="to"
>
<slot />
</button>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { RouterLink, useRoute } from 'vue-router'
const route = useRoute()
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
defineProps<{
to: (() => void) | string
isPrimary?: RouteFunction
isSubpage?: RouteFunction
highlightOverride?: boolean
}>()
defineOptions({
inheritAttrs: false,
})
</script>
<style lang="scss" scoped>
.router-link-active,
.subpage-active {
svg {
filter: drop-shadow(0 0 0.5rem black);
}
}
.router-link-active {
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
}
.subpage-active {
@apply text-contrast bg-button-bg;
}
</style>

View File

@@ -1,160 +0,0 @@
<template>
<nav
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
>
<RouterLink
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
:key="index"
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 ${activeIndex === index && !subpageSelected ? 'text-button-textSelected' : activeIndex === index && subpageSelected ? 'text-contrast' : 'text-primary'}`"
>
<component :is="link.icon" v-if="link.icon" class="size-5" />
<span class="text-nowrap">{{ link.label }}</span>
</RouterLink>
<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 : activeIndex === -1 ? 0 : 1,
}"
aria-hidden="true"
></div>
</nav>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import { useRoute, RouterLink } from 'vue-router'
const route = useRoute()
interface Tab {
label: string
href: string | RouteLocationRaw
shown?: boolean
icon?: unknown
subpages?: string[]
}
const props = defineProps<{
links: Tab[]
query?: string
}>()
const sliderLeft = ref(4)
const sliderTop = ref(4)
const sliderRight = ref(4)
const sliderBottom = ref(4)
const activeIndex = ref(-1)
const oldIndex = ref(-1)
const subpageSelected = ref(false)
const filteredLinks = computed(() =>
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
)
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
const sliderTopPx = computed(() => `${sliderTop.value}px`)
const sliderRightPx = computed(() => `${sliderRight.value}px`)
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
function pickLink() {
let index = -1
subpageSelected.value = false
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
index = i
break
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
index = i
subpageSelected.value = true
break
}
}
activeIndex.value = index
if (activeIndex.value !== -1) {
startAnimation()
} else {
oldIndex.value = -1
sliderLeft.value = 0
sliderRight.value = 0
}
}
const tabLinkElements = ref()
function startAnimation() {
const el = tabLinkElements.value[activeIndex.value].$el
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(() => {
window.addEventListener('resize', pickLink)
pickLink()
})
onUnmounted(() => {
window.removeEventListener('resize', pickLink)
})
watch(route, () => {
pickLink()
})
</script>
<style scoped>
.navtabs-transition {
/* Delay on opacity is to hide any jankiness as the page loads */
transition:
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
}
</style>

View File

@@ -1,121 +0,0 @@
<script setup>
import { Avatar, TagItem } from '@modrinth/ui'
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
import { formatNumber, formatCategory } from '@modrinth/utils'
import { computed } from 'vue'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRouter } from 'vue-router'
dayjs.extend(relativeTime)
const router = useRouter()
const props = defineProps({
project: {
type: Object,
default() {
return {}
},
},
})
const featuredCategory = computed(() => {
if (props.project.categories.includes('optimization')) {
return 'optimization'
}
if (props.project.categories.length > 0) {
return props.project.categories[0]
}
return undefined
})
const toColor = computed(() => {
let color = props.project.color
color >>>= 0
const b = color & 0xff
const g = (color >>> 8) & 0xff
const r = (color >>> 16) & 0xff
return 'rgba(' + [r, g, b, 1].join(',') + ')'
})
const toTransparent = computed(() => {
let color = props.project.color
color >>>= 0
const b = color & 0xff
const g = (color >>> 8) & 0xff
const r = (color >>> 16) & 0xff
return (
'linear-gradient(rgba(' +
[r, g, b, 0.03].join(',') +
'), 65%, rgba(' +
[r, g, b, 0.3].join(',') +
'))'
)
})
</script>
<template>
<div
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
@click="router.push(`/project/${project.slug}`)"
>
<div
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
:style="{
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
'background-image': `url(${
project.featured_gallery ??
project.gallery[0] ??
'https://launcher-files.modrinth.com/assets/maze-bg.png'
})`,
}"
>
<div
class="badges-wrapper"
:class="{
'no-image': !project.featured_gallery && !project.gallery[0],
}"
:style="{
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
}"
></div>
</div>
<div class="flex flex-col justify-center gap-2 px-4 py-3">
<div class="flex gap-2 items-center">
<Avatar size="48px" :src="project.icon_url" />
<div class="h-full flex items-center font-bold text-contrast leading-normal">
<span class="line-clamp-2">{{ project.title }}</span>
</div>
</div>
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
{{ project.description }}
</p>
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
<div
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
>
<DownloadIcon />
{{ formatNumber(project.downloads) }}
</div>
<div
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
>
<HeartIcon />
{{ formatNumber(project.follows) }}
</div>
<div class="flex items-center gap-1 pr-2">
<TagIcon />
<TagItem>
{{ formatCategory(featuredCategory) }}
</TagItem>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,39 +0,0 @@
<script setup>
import { ref, onMounted } from 'vue'
import { init_ads_window } from '@/helpers/ads.js'
const adsWrapper = ref(null)
let devicePixelRatioWatcher = null
function initDevicePixelRatioWatcher() {
if (devicePixelRatioWatcher) {
devicePixelRatioWatcher.removeEventListener('change', updateAdPosition)
}
devicePixelRatioWatcher = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
devicePixelRatioWatcher.addEventListener('change', updateAdPosition)
}
onMounted(() => {
updateAdPosition()
window.addEventListener('resize', updateAdPosition)
initDevicePixelRatioWatcher()
})
function updateAdPosition() {
if (adsWrapper.value) {
init_ads_window()
initDevicePixelRatioWatcher()
}
}
</script>
<template>
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
</div>
</div>
</template>

View File

@@ -1,73 +0,0 @@
<script setup>
import { list } from '@/helpers/profile'
import { handleError } from '@/store/notifications'
import dayjs from 'dayjs'
import { onUnmounted, ref } from 'vue'
import { profile_listener } from '@/helpers/events.js'
import NavButton from '@/components/ui/NavButton.vue'
import { Avatar } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { SpinnerIcon } from '@modrinth/assets'
const recentInstances = ref([])
const getInstances = async () => {
const profiles = await list().catch(handleError)
recentInstances.value = profiles
.sort((a, b) => {
const dateACreated = dayjs(a.created)
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
const dateBCreated = dayjs(b.created)
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
if (dateA.isSame(dateB)) {
return a.name.localeCompare(b.name)
}
return dateB - dateA
})
.slice(0, 4)
}
await getInstances()
const unlistenProfile = await profile_listener(async (event) => {
if (event.event !== 'synced') {
await getInstances()
}
})
onUnmounted(() => {
unlistenProfile()
})
</script>
<template>
<NavButton
v-for="instance in recentInstances"
:key="instance.id"
v-tooltip.right="instance.name"
:to="`/instance/${encodeURIComponent(instance.path)}`"
class="relative"
>
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
size="28px"
:tint-by="instance.path"
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
/>
<div
v-if="instance.install_stage !== 'installed'"
class="absolute inset-0 flex items-center justify-center z-10"
>
<SpinnerIcon class="animate-spin w-4 h-4" />
</div>
</NavButton>
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,175 +0,0 @@
<template>
<div
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
@click="
() => {
emit('open')
$router.push({
path: `/project/${project.project_id ?? project.id}`,
query: { i: props.instance ? props.instance.path : undefined },
})
}
"
>
<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 v-if="project.client_side === 'optional' && project.server_side === 'optional'">
Client or server
</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>
</template>
<script setup>
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
import { ButtonStyled, Avatar } from '@modrinth/ui'
import { formatNumber, formatCategory } from '@modrinth/utils'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { ref, computed } from 'vue'
import { install as installVersion } from '@/store/install.js'
dayjs.extend(relativeTime)
const props = defineProps({
backgroundImage: {
type: String,
default: null,
},
project: {
type: Object,
required: true,
},
categories: {
type: Array,
required: true,
},
instance: {
type: Object,
default: null,
},
featured: {
type: Boolean,
default: false,
},
installed: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['open', 'install'])
const installing = ref(false)
async function install() {
installing.value = true
await installVersion(
props.project.project_id ?? props.project.id,
null,
props.instance ? props.instance.path : null,
'SearchCard',
() => {
installing.value = false
emit('install', props.project.project_id ?? props.project.id)
},
)
}
const modpack = computed(() => props.project.project_type === 'modpack')
</script>

File diff suppressed because one or more lines are too long

View File

@@ -1,100 +0,0 @@
<script setup>
import { Button } from '@modrinth/ui'
import { ref } from 'vue'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get_categories } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js'
import { get_version, get_project } from '@/helpers/cache.js'
import { install as installVersion } from '@/store/install.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const confirmModal = ref(null)
const project = ref(null)
const version = ref(null)
const categories = ref(null)
const installing = ref(false)
defineExpose({
async show(event) {
if (event.event === 'InstallVersion') {
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
project.value = await get_project(version.value.project_id, 'must_revalidate').catch(
handleError,
)
} else {
project.value = await get_project(event.id, 'must_revalidate').catch(handleError)
version.value = await get_version(
project.value.versions[project.value.versions.length - 1],
'must_revalidate',
).catch(handleError)
}
categories.value = (await get_categories().catch(handleError)).filter(
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
)
confirmModal.value.show()
},
})
async function install() {
confirmModal.value.hide()
await installVersion(project.value.id, version.value.id, null, 'URLConfirmModal')
}
</script>
<template>
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
<div class="modal-body">
<SearchCard
:project="project"
class="project-card"
:categories="categories"
@open="confirmModal.hide()"
/>
<div class="button-row">
<div class="markdown-body">
<p>
Installing <code>{{ version.id }}</code> from Modrinth
</p>
</div>
<div class="button-group">
<Button :loading="installing" color="primary" @click="install">Install</Button>
</div>
</div>
</div>
</ModalWrapper>
</template>
<style scoped lang="scss">
.modal-body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--gap-md);
}
.button-row {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--gap-md);
}
.button-group {
display: flex;
flex-direction: row;
gap: var(--gap-sm);
}
.project-card {
background-color: var(--color-bg);
width: 100%;
:deep(.badge) {
border: 1px solid var(--color-raised-bg);
background-color: var(--color-accent-contrast);
}
}
</style>

View File

@@ -1,357 +0,0 @@
<script setup lang="ts">
import { Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
import {
UserPlusIcon,
MoreVerticalIcon,
MailIcon,
SettingsIcon,
TrashIcon,
XIcon,
} from '@modrinth/assets'
import { ref, onUnmounted, watch, computed } from 'vue'
import { friend_listener } from '@/helpers/events'
import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
import { get_user_many } from '@/helpers/cache'
import { handleError } from '@/store/notifications.js'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const props = defineProps<{
credentials: unknown | null
signIn: () => void
}>()
const userCredentials = computed(() => props.credentials)
const search = ref('')
const manageFriendsModal = ref()
const friendInvitesModal = ref()
const username = ref('')
const addFriendModal = ref()
async function addFriendFromModal() {
addFriendModal.value.hide()
await add_friend(username.value).catch(handleError)
username.value = ''
await loadFriends()
}
const friendOptions = ref()
async function handleFriendOptions(args) {
switch (args.option) {
case 'remove-friend':
await removeFriend(args.item)
break
}
}
async function addFriend(friend: Friend) {
await add_friend(
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
).catch(handleError)
await loadFriends()
}
async function removeFriend(friend: Friend) {
await remove_friend(
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
).catch(handleError)
await loadFriends()
}
type Friend = {
id: string
friend_id: string | null
status: string | null
last_updated: Dayjs | null
created: Dayjs
username: string
accepted: boolean
online: boolean
avatar: string
}
const userFriends = ref<Friend[]>([])
const acceptedFriends = computed(() =>
userFriends.value
.filter((x) => x.accepted)
.toSorted((a, b) => {
if (a.last_updated === null && b.last_updated === null) {
return 0 // Both are null, equal in sorting
}
if (a.last_updated === null) {
return 1 // `a` is null, move it after `b`
}
if (b.last_updated === null) {
return -1 // `b` is null, move it after `a`
}
// Both are non-null, sort by date
return b.last_updated.diff(a.last_updated)
}),
)
const pendingFriends = computed(() =>
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
)
const loading = ref(true)
async function loadFriends(timeout = false) {
loading.value = timeout
try {
const friendsList = await friends()
if (friendsList.length === 0) {
userFriends.value = []
} else {
const friendStatuses = await friend_statuses()
const users = await get_user_many(
friendsList.map((x) => (x.id === userCredentials.value.user_id ? x.friend_id : x.id)),
)
userFriends.value = friendsList.map((friend) => {
const user = users.find((x) => x.id === friend.id || x.id === friend.friend_id)
const status = friendStatuses.find(
(x) => x.user_id === friend.id || x.user_id === friend.friend_id,
)
return {
id: friend.id,
friend_id: friend.friend_id,
status: status?.profile_name,
last_updated: status && status.last_update ? dayjs(status.last_update) : null,
created: dayjs(friend.created),
avatar: user?.avatar_url,
username: user?.username,
online: !!status,
accepted: friend.accepted,
}
})
}
loading.value = false
} catch (e) {
console.error('Error loading friends', e)
if (timeout) {
setTimeout(() => loadFriends(), 15 * 1000)
}
}
}
watch(
userCredentials,
() => {
if (userCredentials.value === undefined) {
userFriends.value = []
} else if (userCredentials.value === null) {
userFriends.value = []
loading.value = false
} else {
loadFriends(true)
}
},
{ immediate: true },
)
const unlisten = await friend_listener(() => loadFriends())
onUnmounted(() => {
unlisten()
})
</script>
<template>
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
<div
v-for="friend in acceptedFriends.filter(
(x) => !search || x.username.toLowerCase().includes(search),
)"
:key="friend.username"
class="flex gap-2 items-center"
>
<div class="relative">
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
<span
v-if="friend.online"
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
/>
</div>
<div>{{ friend.username }}</div>
<div class="ml-auto">
<ButtonStyled>
<button @click="removeFriend(friend)">
<XIcon />
Remove
</button>
</ButtonStyled>
</div>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
<p v-if="pendingFriends.length === 0">You have no pending friend requests :C</p>
<div v-else class="flex flex-col gap-4">
<div v-for="friend in pendingFriends" :key="friend.username" class="flex gap-2">
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
<div class="flex flex-col gap-2">
<div>
<p class="m-0">
<template v-if="friend.id === userCredentials.user_id">
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
</template>
<template v-else>
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
</template>
</p>
<p class="m-0 text-sm text-secondary">{{ friend.created.fromNow() }}</p>
</div>
<div class="flex gap-2">
<template v-if="friend.id === userCredentials.user_id">
<ButtonStyled color="brand">
<button @click="addFriend(friend)">
<UserPlusIcon />
Accept
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="removeFriend(friend)">
<XIcon />
Ignore
</button>
</ButtonStyled>
</template>
<template v-else>
<ButtonStyled>
<button @click="removeFriend(friend)">
<XIcon />
Cancel
</button>
</ButtonStyled>
</template>
</div>
</div>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="addFriendModal" header="Add a friend">
<div class="mb-4">
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
</div>
<ButtonStyled color="brand">
<button :disabled="username.length === 0" @click="addFriendFromModal">
<UserPlusIcon />
Add friend
</button>
</ButtonStyled>
</ModalWrapper>
<div class="flex justify-between items-center">
<h3 class="text-lg m-0">Friends</h3>
<ButtonStyled v-if="userCredentials" type="transparent" circular>
<OverflowMenu
:options="[
{
id: 'add-friend',
action: () => addFriendModal.show(),
},
{
id: 'manage-friends',
action: () => manageFriendsModal.show(),
shown: acceptedFriends.length > 0,
},
{
id: 'view-requests',
action: () => friendInvitesModal.show(),
shown: pendingFriends.length > 0,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #add-friend>
<UserPlusIcon aria-hidden="true" />
Add friend
</template>
<template #manage-friends>
<SettingsIcon aria-hidden="true" />
Manage friends
<div
v-if="acceptedFriends.length > 0"
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
>
{{ acceptedFriends.length }}
</div>
</template>
<template #view-requests>
<MailIcon aria-hidden="true" />
View friend requests
<div
v-if="pendingFriends.length > 0"
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
>
{{ pendingFriends.length }}
</div>
</template>
</OverflowMenu>
</ButtonStyled>
</div>
<div class="flex flex-col gap-2 mt-2">
<template v-if="loading">
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
<div class="flex flex-col w-full">
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
</div>
</div>
</template>
<template v-else-if="acceptedFriends.length === 0">
<div class="text-sm">
<div v-if="!userCredentials">
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
</div>
<div v-else>
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
to share what you're playing!
</div>
</div>
</template>
<template v-else>
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
<template #remove-friend> <TrashIcon /> Remove friend </template>
</ContextMenu>
<div
v-for="friend in acceptedFriends.slice(0, 5)"
:key="friend.username"
class="flex gap-2 items-center"
:class="{ grayscale: !friend.online }"
@contextmenu.prevent.stop="
(event) =>
friendOptions.showMenu(event, friend, [
{
name: 'remove-friend',
color: 'danger',
},
])
"
>
<div class="relative">
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
<span
v-if="friend.online"
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
/>
</div>
<div class="flex flex-col">
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
{{ friend.username }}
</span>
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
</div>
</div>
</template>
</div>
</template>

View File

@@ -1,166 +0,0 @@
<template>
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
<div class="modal-body">
<p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
installed.
</p>
<table>
<thead>
<tr class="header">
<th>{{ instance?.name }}</th>
<th>{{ project.title }}</th>
</tr>
</thead>
<tbody>
<tr class="content">
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
<td>
<multiselect
v-if="versions?.length > 1"
v-model="selectedVersion"
:options="versions"
:searchable="true"
placeholder="Select version"
open-direction="top"
:show-labels="false"
:custom-label="
(version) =>
`${version?.name} (${version?.loaders
.map((name) => formatCategory(name))
.join(', ')} - ${version?.game_versions.join(', ')})`
"
:max-height="150"
/>
<span v-else>
<span>
{{ selectedVersion?.name }} ({{
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
}}
- {{ selectedVersion?.game_versions.join(', ') }})
</span>
</span>
</td>
</tr>
</tbody>
</table>
<div class="button-group">
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
<Button color="primary" :disabled="installing" @click="install()">
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
</Button>
</div>
</div>
</ModalWrapper>
</template>
<script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { XIcon, DownloadIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui'
import { formatCategory } from '@modrinth/utils'
import { add_project_from_version as installMod } from '@/helpers/profile'
import { ref } from 'vue'
import { handleError } from '@/store/state.js'
import { trackEvent } from '@/helpers/analytics'
import Multiselect from 'vue-multiselect'
const instance = ref(null)
const project = ref(null)
const versions = ref(null)
const selectedVersion = ref(null)
const incompatibleModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
defineExpose({
show: (instanceVal, projectVal, projectVersions, callback) => {
instance.value = instanceVal
versions.value = projectVersions
selectedVersion.value = projectVersions[0]
project.value = projectVal
onInstall.value = callback
installing.value = false
incompatibleModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
})
const install = async () => {
installing.value = true
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
installing.value = false
onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide()
trackEvent('ProjectInstall', {
loader: instance.value.loader,
game_version: instance.value.game_version,
id: project.value,
version_id: selectedVersion.value.id,
project_type: project.value.project_type,
title: project.value.title,
source: 'ProjectIncompatibilityWarningModal',
})
}
</script>
<style lang="scss" scoped>
.data {
text-transform: capitalize;
}
table {
width: 100%;
border-radius: var(--radius-lg);
border-collapse: collapse;
box-shadow: 0 0 0 1px var(--color-button-bg);
}
th {
text-align: left;
padding: 1rem;
background-color: var(--color-bg);
overflow: hidden;
border-bottom: 1px solid var(--color-button-bg);
}
th:first-child {
border-top-left-radius: var(--radius-lg);
border-right: 1px solid var(--color-button-bg);
}
th:last-child {
border-top-right-radius: var(--radius-lg);
}
td {
padding: 1rem;
}
td:first-child {
border-right: 1px solid var(--color-button-bg);
}
.button-group {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
:deep(.animated-dropdown .options) {
max-height: 13.375rem;
}
}
</style>

View File

@@ -1,72 +0,0 @@
<script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui'
import { create_profile_and_install as pack_install } from '@/helpers/pack'
import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { handleError } from '@/store/state.js'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
const versionId = ref()
const project = ref()
const confirmModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
defineExpose({
show: (projectVal, versionIdVal, callback) => {
project.value = projectVal
versionId.value = versionIdVal
installing.value = false
confirmModal.value.show()
onInstall.value = callback
trackEvent('PackInstallStart')
},
})
async function install() {
installing.value = true
confirmModal.value.hide()
await pack_install(
project.value.id,
versionId.value,
project.value.title,
project.value.icon_url,
).catch(handleError)
trackEvent('PackInstall', {
id: project.value.id,
version_id: versionId.value,
title: project.value.title,
source: 'ConfirmModal',
})
onInstall.value(versionId.value)
installing.value = false
}
</script>
<template>
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
<div class="modal-body">
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right">
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
<Button color="primary" :disabled="installing" @click="install()"
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
>
</div>
</div>
</ModalWrapper>
</template>
<style lang="scss" scoped>
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -1,326 +0,0 @@
<script setup lang="ts">
import { convertFileSrc } from '@tauri-apps/api/core'
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
import { computed, ref, type Ref, watch } from 'vue'
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
import { handleError } from '@/store/notifications'
import { trackEvent } from '@/helpers/analytics'
import { open } from '@tauri-apps/plugin-dialog'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { useRouter } from 'vue-router'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
const { formatMessage } = useVIntl()
const router = useRouter()
const deleteConfirmModal = ref()
const props = defineProps<InstanceSettingsTabProps>()
const title = ref(props.instance.name)
const icon: Ref<string | undefined> = ref(props.instance.icon_path)
const groups = ref(props.instance.groups)
const newCategoryInput = ref('')
const installing = computed(() => props.instance.install_stage !== 'installed')
async function duplicateProfile() {
await duplicate(props.instance.path).catch(handleError)
trackEvent('InstanceDuplicate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
const allInstances = ref((await list()) as GameInstance[])
const availableGroups = computed(() => [
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
])
async function resetIcon() {
icon.value = undefined
await edit_icon(props.instance.path, null).catch(handleError)
trackEvent('InstanceRemoveIcon')
}
async function setIcon() {
const value = await open({
multiple: false,
filters: [
{
name: 'Image',
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
},
],
})
if (!value) return
icon.value = value
await edit_icon(props.instance.path, icon.value).catch(handleError)
trackEvent('InstanceSetIcon')
}
const editProfileObject = computed(() => ({
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
}))
const toggleGroup = (group: string) => {
if (groups.value.includes(group)) {
groups.value = groups.value.filter((x) => x !== group)
} else {
groups.value.push(group)
}
}
const addCategory = () => {
const text = newCategoryInput.value.trim()
if (text.length > 0) {
groups.value.push(text.substring(0, 32))
newCategoryInput.value = ''
}
}
watch(
[title, groups, groups],
async () => {
await edit(props.instance.path, editProfileObject.value)
},
{ deep: true },
)
const removing = ref(false)
async function removeProfile() {
removing.value = true
await remove(props.instance.path).catch(handleError)
removing.value = false
trackEvent('InstanceRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
await router.push({ path: '/' })
}
const messages = defineMessages({
name: {
id: 'instance.settings.tabs.general.name',
defaultMessage: 'Name',
},
libraryGroups: {
id: 'instance.settings.tabs.general.library-groups',
defaultMessage: 'Library groups',
},
libraryGroupsDescription: {
id: 'instance.settings.tabs.general.library-groups.description',
defaultMessage:
'Library groups allow you to organize your instances into different sections in your library.',
},
libraryGroupsEnterName: {
id: 'instance.settings.tabs.general.library-groups.enter-name',
defaultMessage: 'Enter group name',
},
libraryGroupsCreate: {
id: 'instance.settings.tabs.general.library-groups.create',
defaultMessage: 'Create new group',
},
editIcon: {
id: 'instance.settings.tabs.general.edit-icon',
defaultMessage: 'Edit icon',
},
selectIcon: {
id: 'instance.settings.tabs.general.edit-icon.select',
defaultMessage: 'Select icon',
},
replaceIcon: {
id: 'instance.settings.tabs.general.edit-icon.replace',
defaultMessage: 'Replace icon',
},
removeIcon: {
id: 'instance.settings.tabs.general.edit-icon.remove',
defaultMessage: 'Remove icon',
},
duplicateInstance: {
id: 'instance.settings.tabs.general.duplicate-instance',
defaultMessage: 'Duplicate instance',
},
duplicateInstanceDescription: {
id: 'instance.settings.tabs.general.duplicate-instance.description',
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
},
duplicateButtonTooltipInstalling: {
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
defaultMessage: 'Cannot duplicate while installing.',
},
duplicateButton: {
id: 'instance.settings.tabs.general.duplicate-button',
defaultMessage: 'Duplicate',
},
deleteInstance: {
id: 'instance.settings.tabs.general.delete',
defaultMessage: 'Delete instance',
},
deleteInstanceDescription: {
id: 'instance.settings.tabs.general.delete.description',
defaultMessage:
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
},
deleteInstanceButton: {
id: 'instance.settings.tabs.general.delete.button',
defaultMessage: 'Delete instance',
},
deletingInstanceButton: {
id: 'instance.settings.tabs.general.deleting.button',
defaultMessage: 'Deleting...',
},
})
</script>
<template>
<ConfirmModalWrapper
ref="deleteConfirmModal"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
:show-ad-on-close="false"
@proceed="removeProfile"
/>
<div class="block">
<div class="float-end ml-4 relative group">
<OverflowMenu
v-tooltip="formatMessage(messages.editIcon)"
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
:options="[
{
id: 'select',
action: () => setIcon(),
},
{
id: 'remove',
color: 'danger',
action: () => resetIcon(),
shown: !!icon,
},
]"
>
<Avatar
:src="icon ? convertFileSrc(icon) : icon"
size="108px"
class="!border-4 group-hover:brightness-75"
:tint-by="props.instance.path"
no-shadow
/>
<div class="absolute top-0 right-0 m-2">
<div
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
>
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
</div>
</div>
<template #select>
<UploadIcon />
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
</template>
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
</OverflowMenu>
</div>
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.name) }}
</label>
<div class="flex">
<input
id="instance-name"
v-model="title"
autocomplete="off"
maxlength="80"
class="flex-grow"
type="text"
/>
</div>
<template v-if="instance.install_stage == 'installed'">
<div>
<h2
id="duplicate-instance-label"
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
>
{{ formatMessage(messages.duplicateInstance) }}
</h2>
<p class="m-0 mb-2">
{{ formatMessage(messages.duplicateInstanceDescription) }}
</p>
</div>
<ButtonStyled>
<button
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
aria-labelledby="duplicate-instance-label"
:disabled="installing"
@click="duplicateProfile"
>
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
</button>
</ButtonStyled>
</template>
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.libraryGroups) }}
</h2>
<p class="m-0 mb-2">
{{ formatMessage(messages.libraryGroupsDescription) }}
</p>
<div class="flex flex-col gap-1">
<Checkbox
v-for="group in availableGroups"
:key="group"
:model-value="groups.includes(group)"
:label="group"
@click="toggleGroup(group)"
/>
<div class="flex gap-2 items-center">
<input
v-model="newCategoryInput"
type="text"
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
@submit="() => addCategory"
/>
<ButtonStyled>
<button class="w-fit" @click="() => addCategory()">
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
</button>
</ButtonStyled>
</div>
</div>
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.deleteInstance) }}
</h2>
<p class="m-0 mb-2">
{{ formatMessage(messages.deleteInstanceDescription) }}
</p>
<ButtonStyled color="red">
<button
aria-labelledby="delete-instance-label"
:disabled="removing"
@click="deleteConfirmModal.show()"
>
<SpinnerIcon v-if="removing" class="animate-spin" />
<TrashIcon v-else />
{{
removing
? formatMessage(messages.deletingInstanceButton)
: formatMessage(messages.deleteInstanceButton)
}}
</button>
</ButtonStyled>
</div>
</template>
<style scoped lang="scss">
.hovering-icon-shadow {
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
}
</style>

View File

@@ -1,153 +0,0 @@
<script setup lang="ts">
import { Checkbox } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { get } from '@/helpers/settings'
import { edit } from '@/helpers/profile'
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const globalSettings = (await get().catch(handleError)) as AppSettings
const overrideHooks = ref(
!!props.instance.hooks.pre_launch ||
!!props.instance.hooks.wrapper ||
!!props.instance.hooks.post_exit,
)
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
const editProfileObject = computed(() => {
const editProfile: {
hooks?: Hooks
} = {}
if (overrideHooks.value) {
editProfile.hooks = hooks.value
}
return editProfile
})
watch(
[overrideHooks, hooks],
async () => {
await edit(props.instance.path, editProfileObject.value)
},
{ deep: true },
)
const messages = defineMessages({
hooks: {
id: 'instance.settings.tabs.hooks.title',
defaultMessage: 'Game launch hooks',
},
hooksDescription: {
id: 'instance.settings.tabs.hooks.description',
defaultMessage:
'Hooks allow advanced users to run certain system commands before and after launching the game.',
},
customHooks: {
id: 'instance.settings.tabs.hooks.custom-hooks',
defaultMessage: 'Custom launch hooks',
},
preLaunch: {
id: 'instance.settings.tabs.hooks.pre-launch',
defaultMessage: 'Pre-launch',
},
preLaunchDescription: {
id: 'instance.settings.tabs.hooks.pre-launch.description',
defaultMessage: 'Ran before the instance is launched.',
},
preLaunchEnter: {
id: 'instance.settings.tabs.hooks.pre-launch.enter',
defaultMessage: 'Enter pre-launch command...',
},
wrapper: {
id: 'instance.settings.tabs.hooks.wrapper',
defaultMessage: 'Wrapper',
},
wrapperDescription: {
id: 'instance.settings.tabs.hooks.wrapper.description',
defaultMessage: 'Wrapper command for launching Minecraft.',
},
wrapperEnter: {
id: 'instance.settings.tabs.hooks.wrapper.enter',
defaultMessage: 'Enter wrapper command...',
},
postExit: {
id: 'instance.settings.tabs.hooks.post-exit',
defaultMessage: 'Post-exit',
},
postExitDescription: {
id: 'instance.settings.tabs.hooks.post-exit.description',
defaultMessage: 'Ran after the game closes.',
},
postExitEnter: {
id: 'instance.settings.tabs.hooks.post-exit.enter',
defaultMessage: 'Enter post-exit command...',
},
})
</script>
<template>
<div>
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
{{ formatMessage(messages.hooks) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.hooksDescription) }}
</p>
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
{{ formatMessage(messages.preLaunch) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.preLaunchDescription) }}
</p>
<input
id="pre-launch"
v-model="hooks.pre_launch"
autocomplete="off"
:disabled="!overrideHooks"
type="text"
:placeholder="formatMessage(messages.preLaunchEnter)"
class="w-full mt-2"
/>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
{{ formatMessage(messages.wrapper) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.wrapperDescription) }}
</p>
<input
id="wrapper"
v-model="hooks.wrapper"
autocomplete="off"
:disabled="!overrideHooks"
type="text"
:placeholder="formatMessage(messages.wrapperEnter)"
class="w-full mt-2"
/>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
{{ formatMessage(messages.postExit) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.postExitDescription) }}
</p>
<input
id="post-exit"
v-model="hooks.post_exit"
autocomplete="off"
:disabled="!overrideHooks"
type="text"
:placeholder="formatMessage(messages.postExitEnter)"
class="w-full mt-2"
/>
</div>
</template>

View File

@@ -1,789 +0,0 @@
<script setup lang="ts">
import {
TransferIcon,
IssuesIcon,
HammerIcon,
DownloadIcon,
WrenchIcon,
UndoIcon,
SpinnerIcon,
UnplugIcon,
UnlinkIcon,
} from '@modrinth/assets'
import { Avatar, Checkbox, Chips, ButtonStyled, TeleportDropdownMenu } from '@modrinth/ui'
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
import { handleError } from '@/store/notifications'
import { trackEvent } from '@/helpers/analytics'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { get_loader_versions } from '@/helpers/metadata'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import {
formatCategory,
type GameVersionTag,
type PlatformTag,
type Project,
type Version,
} from '@modrinth/utils'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { get_project, get_version_many } from '@/helpers/cache'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import dayjs from 'dayjs'
import type {
InstanceSettingsTabProps,
ManifestLoaderVersion,
Manifest,
} from '../../../helpers/types'
const { formatMessage } = useVIntl()
const repairConfirmModal = ref()
const modpackVersionModal = ref()
const modalConfirmUnpair = ref()
const modalConfirmReinstall = ref()
const props = defineProps<InstanceSettingsTabProps>()
const loader = ref(props.instance.loader)
const gameVersion = ref(props.instance.game_version)
const showSnapshots = ref(false)
const [
fabric_versions,
forge_versions,
quilt_versions,
neoforge_versions,
all_game_versions,
loaders,
] = await Promise.all([
get_loader_versions('fabric')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('forge')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('quilt')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('neo')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_game_versions()
.then((gameVersions: GameVersionTag[]) => shallowRef(gameVersions))
.catch(handleError),
get_loaders()
.then((value: PlatformTag[]) =>
value
.filter(
(item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla',
)
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
.then((loader: PlatformTag[]) => ref(loader))
.catch(handleError),
])
const modpackProject: Ref<Project | null> = ref(null)
const modpackVersion: Ref<Version | null> = ref(null)
const modpackVersions: Ref<Version[] | null> = ref(null)
const fetching = ref(true)
if (props.instance.linked_data && props.instance.linked_data.project_id && !props.offline) {
get_project(props.instance.linked_data.project_id, 'must_revalidate')
.then((project) => {
modpackProject.value = project
if (project && project.versions) {
get_version_many(project.versions, 'must_revalidate')
.then((versions: Version[]) => {
modpackVersions.value = versions.sort((a, b) =>
dayjs(b.date_published).diff(dayjs(a.date_published)),
)
modpackVersion.value =
versions.find(
(version: Version) => version.id === props.instance.linked_data?.version_id,
) ?? null
})
.catch(handleError)
.finally(() => {
fetching.value = false
})
}
})
.catch((err) => {
handleError(err)
fetching.value = false
})
} else {
fetching.value = false
}
const currentLoaderIcon = computed(
() => loaders?.value.find((x) => x.name === props.instance.loader)?.icon,
)
const gameVersionsForLoader = computed(() => {
return all_game_versions?.value.filter((item) => {
if (loader.value === 'fabric') {
return !!fabric_versions?.value.gameVersions.some((x) => item.version === x.id)
} else if (loader.value === 'forge') {
return !!forge_versions?.value.gameVersions.some((x) => item.version === x.id)
} else if (loader.value === 'quilt') {
return !!quilt_versions?.value.gameVersions.some((x) => item.version === x.id)
} else if (loader.value === 'neoforge') {
return !!neoforge_versions?.value.gameVersions.some((x) => item.version === x.id)
}
return []
})
})
const hasSnapshots = computed(() =>
gameVersionsForLoader.value?.some((x) => x.version_type !== 'release'),
)
const selectableGameVersionNumbers = computed(() => {
return gameVersionsForLoader.value
?.filter((x) => x.version_type === 'release' || showSnapshots.value)
.map((x) => x.version)
})
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
if (gameVersion.value) {
if (loader.value === 'fabric') {
return fabric_versions?.value.gameVersions[0].loaders
} else if (loader.value === 'forge') {
return forge_versions?.value?.gameVersions?.find((item) => item.id === gameVersion.value)
?.loaders
} else if (loader.value === 'quilt') {
return quilt_versions?.value.gameVersions[0].loaders
} else if (loader.value === 'neoforge') {
return neoforge_versions?.value?.gameVersions?.find((item) => item.id === gameVersion.value)
?.loaders
}
}
return []
})
const loaderVersionIndex: Ref<number> = ref(-1)
resetLoaderVersionIndex()
function resetLoaderVersionIndex() {
loaderVersionIndex.value =
selectableLoaderVersions.value?.findIndex((x) => x.id === props.instance.loader_version) ?? -1
}
const isValid = computed(() => {
return (
selectableGameVersionNumbers.value?.includes(gameVersion.value) &&
((loaderVersionIndex.value !== undefined && loaderVersionIndex.value >= 0) ||
loader.value === 'vanilla')
)
})
const isChanged = computed(() => {
return (
loader.value !== props.instance.loader ||
gameVersion.value !== props.instance.game_version ||
(loader.value !== 'vanilla' &&
loaderVersionIndex.value !== undefined &&
loaderVersionIndex.value >= 0 &&
selectableLoaderVersions.value?.[loaderVersionIndex.value].id !==
props.instance.loader_version)
)
})
watch(loader, () => {
loaderVersionIndex.value = 0
})
const editing = ref(false)
async function saveGvLoaderEdits() {
editing.value = true
const editProfile: { loader?: string; game_version?: string; loader_version?: string } = {}
editProfile.loader = loader.value
editProfile.game_version = gameVersion.value
if (loader.value !== 'vanilla' && loaderVersionIndex.value !== undefined) {
editProfile.loader_version = selectableLoaderVersions.value?.[loaderVersionIndex.value].id
} else {
loaderVersionIndex.value = -1
}
console.log('Editing:')
console.log(loader.value)
await edit(props.instance.path, editProfile).catch(handleError)
await repairProfile(false)
editing.value = false
}
const installing = computed(() => props.instance.install_stage !== 'installed')
const repairing = ref(false)
const reinstalling = ref(false)
const changingVersion = ref(false)
async function repairProfile(force: boolean) {
if (force) {
repairing.value = true
}
await install(props.instance.path, force).catch(handleError)
if (force) {
repairing.value = false
}
trackEvent('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
async function unpairProfile() {
await edit(props.instance.path, {
linked_data: null,
})
modpackProject.value = null
modpackVersion.value = null
modpackVersions.value = null
modalConfirmUnpair.value.hide()
}
async function repairModpack() {
reinstalling.value = true
await update_repair_modrinth(props.instance.path).catch(handleError)
reinstalling.value = false
trackEvent('InstanceRepair', {
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
const messages = defineMessages({
cannotWhileInstalling: {
id: 'instance.settings.tabs.installation.tooltip.cannot-while-installing',
defaultMessage: 'Cannot {action} while installing',
},
cannotWhileOffline: {
id: 'instance.settings.tabs.installation.tooltip.cannot-while-offline',
defaultMessage: 'Cannot {action} while offline',
},
cannotWhileRepairing: {
id: 'instance.settings.tabs.installation.tooltip.cannot-while-repairing',
defaultMessage: 'Cannot {action} while repairing',
},
currentlyInstalled: {
id: 'instance.settings.tabs.installation.currently-installed',
defaultMessage: 'Currently installed',
},
platform: {
id: 'instance.settings.tabs.installation.platform',
defaultMessage: 'Platform',
},
gameVersion: {
id: 'instance.settings.tabs.installation.game-version',
defaultMessage: 'Game version',
},
loaderVersion: {
id: 'instance.settings.tabs.installation.loader-version',
defaultMessage: '{loader} version',
},
showAllVersions: {
id: 'instance.settings.tabs.installation.show-all-versions',
defaultMessage: 'Show all versions',
},
install: {
id: 'instance.settings.tabs.installation.install',
defaultMessage: 'Install',
},
resetSelections: {
id: 'instance.settings.tabs.installation.reset-selections',
defaultMessage: 'Reset to current',
},
unknownVersion: {
id: 'instance.settings.tabs.installation.unknown-version',
defaultMessage: '(unknown version)',
},
repairConfirmTitle: {
id: 'instance.settings.tabs.installation.repair.confirm.title',
defaultMessage: 'Repair instance?',
},
repairConfirmDescription: {
id: 'instance.settings.tabs.installation.repair.confirm.description',
defaultMessage:
'Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods.',
},
repairButton: {
id: 'instance.settings.tabs.installation.repair.button',
defaultMessage: 'Repair',
},
repairingButton: {
id: 'instance.settings.tabs.installation.repair.button.repairing',
defaultMessage: 'Repairing',
},
repairInProgress: {
id: 'instance.settings.tabs.installation.repair.in-progress',
defaultMessage: 'Repair in progress',
},
repairAction: {
id: 'instance.settings.tabs.installation.tooltip.action.repair',
defaultMessage: 'repair',
},
changeVersionCannotWhileFetching: {
id: 'instance.settings.tabs.installation.change-version.cannot-while-fetching',
defaultMessage: 'Fetching modpack versions',
},
changeVersionButton: {
id: 'instance.settings.tabs.installation.change-version.button',
defaultMessage: 'Change version',
},
changeVersionAction: {
id: 'instance.settings.tabs.installation.tooltip.action.change-version',
defaultMessage: 'change version',
},
installingButton: {
id: 'instance.settings.tabs.installation.change-version.button.installing',
defaultMessage: 'Installing',
},
installInProgress: {
id: 'instance.settings.tabs.installation.install.in-progress',
defaultMessage: 'Installation in progress',
},
installButton: {
id: 'instance.settings.tabs.installation.change-version.button.install',
defaultMessage: 'Install',
},
alreadyInstalledVanilla: {
id: 'instance.settings.tabs.installation.change-version.already-installed.vanilla',
defaultMessage: 'Vanilla {game_version} already installed',
},
alreadyInstalledModded: {
id: 'instance.settings.tabs.installation.change-version.already-installed.modded',
defaultMessage: '{platform} {version} for Minecraft {game_version} already installed',
},
installAction: {
id: 'instance.settings.tabs.installation.tooltip.action.install',
defaultMessage: 'install',
},
installingNewVersion: {
id: 'instance.settings.tabs.installation.change-version.in-progress',
defaultMessage: 'Installing new version',
},
minecraftVersion: {
id: 'instance.settings.tabs.installation.minecraft-version',
defaultMessage: 'Minecraft {version}',
},
noLoaderVersions: {
id: 'instance.settings.tabs.installation.no-loader-versions',
defaultMessage: '{loader} is not available for Minecraft {version}. Try another mod loader.',
},
noConnection: {
id: 'instance.settings.tabs.installation.no-connection',
defaultMessage: 'Cannot fetch linked modpack details. Please check your internet connection.',
},
noModpackFound: {
id: 'instance.settings.tabs.installation.no-modpack-found',
defaultMessage:
'This instance is linked to a modpack, but the modpack could not be found on Modrinth.',
},
debugInformation: {
id: 'instance.settings.tabs.installation.debug-information',
defaultMessage: 'Debug information:',
},
fetchingModpackDetails: {
id: 'instance.settings.tabs.installation.fetching-modpack-details',
defaultMessage: 'Fetching modpack details',
},
unlinkInstanceTitle: {
id: 'instance.settings.tabs.installation.unlink.title',
defaultMessage: 'Unlink from modpack',
},
unlinkInstanceDescription: {
id: 'instance.settings.tabs.installation.unlink.description',
defaultMessage: `This instance is linked to a modpack, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the modpack.`,
},
unlinkInstanceButton: {
id: 'instance.settings.tabs.installation.unlink.button',
defaultMessage: 'Unlink instance',
},
unlinkInstanceConfirmTitle: {
id: 'instance.settings.tabs.installation.unlink.confirm.title',
defaultMessage: 'Are you sure you want to unlink this instance?',
},
unlinkInstanceConfirmDescription: {
id: 'instance.settings.tabs.installation.unlink.confirm.description',
defaultMessage:
'If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal.',
},
reinstallModpackConfirmTitle: {
id: 'instance.settings.tabs.installation.reinstall.confirm.title',
defaultMessage: 'Are you sure you want to reinstall this instance?',
},
reinstallModpackConfirmDescription: {
id: 'instance.settings.tabs.installation.reinstall.confirm.description',
defaultMessage: `Reinstalling will reset all installed or modified content to what is provided by the modpack, removing any mods or content you have added on top of the original installation. This may fix unexpected behavior if changes have been made to the instance, but if your worlds now depend on additional installed content, it may break existing worlds.`,
},
reinstallModpackTitle: {
id: 'instance.settings.tabs.installation.reinstall.title',
defaultMessage: 'Reinstall modpack',
},
reinstallModpackDescription: {
id: 'instance.settings.tabs.installation.reinstall.description',
defaultMessage: `Resets the instance's content to its original state, removing any mods or content you have added on top of the original modpack.`,
},
reinstallModpackButton: {
id: 'instance.settings.tabs.installation.reinstall.button',
defaultMessage: 'Reinstall modpack',
},
reinstallingModpackButton: {
id: 'instance.settings.tabs.installation.reinstall.button.reinstalling',
defaultMessage: 'Reinstalling modpack',
},
reinstallAction: {
id: 'instance.settings.tabs.installation.tooltip.action.reinstall',
defaultMessage: 'reinstall',
},
})
</script>
<template>
<ConfirmModalWrapper
ref="repairConfirmModal"
:title="formatMessage(messages.repairConfirmTitle)"
:description="formatMessage(messages.repairConfirmDescription)"
:proceed-icon="HammerIcon"
:proceed-label="formatMessage(messages.repairButton)"
:danger="false"
:show-ad-on-close="false"
@proceed="() => repairProfile(true)"
/>
<ModpackVersionModal
v-if="instance.linked_data && modpackVersions"
ref="modpackVersionModal"
:instance="instance"
:versions="modpackVersions"
@finish-install="
() => {
changingVersion = false
modpackVersion =
modpackVersions?.find(
(version: Version) => version.id === props.instance.linked_data?.version_id,
) ?? null
}
"
/>
<ConfirmModalWrapper
ref="modalConfirmUnpair"
:title="formatMessage(messages.unlinkInstanceConfirmTitle)"
:description="formatMessage(messages.unlinkInstanceConfirmDescription)"
:proceed-icon="UnlinkIcon"
:proceed-label="formatMessage(messages.unlinkInstanceButton)"
:show-ad-on-close="false"
@proceed="() => unpairProfile()"
/>
<ConfirmModalWrapper
ref="modalConfirmReinstall"
:title="formatMessage(messages.reinstallModpackConfirmTitle)"
:description="formatMessage(messages.reinstallModpackConfirmDescription)"
:proceed-icon="DownloadIcon"
:proceed-label="formatMessage(messages.reinstallModpackButton)"
:show-ad-on-close="false"
@proceed="() => repairModpack()"
/>
<div>
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.currentlyInstalled) }}
</h2>
<div
v-if="!modpackProject && instance.linked_data && offline && !fetching"
class="text-secondary font-medium mb-2"
>
<UnplugIcon class="top-[3px] relative" /> {{ formatMessage(messages.noConnection) }}
</div>
<div v-else-if="!modpackProject && instance.linked_data && !fetching" class="mb-2">
<p class="text-brand-red font-medium mt-0">
<IssuesIcon class="top-[3px] relative" /> {{ formatMessage(messages.noModpackFound) }}
</p>
<p>{{ formatMessage(messages.debugInformation) }}</p>
<div class="bg-bg p-6 rounded-2xl mt-2 text-sm text-secondary">
{{ instance.linked_data }}
</div>
</div>
<div class="flex gap-4 items-center justify-between p-4 bg-bg rounded-2xl">
<div v-if="fetching" class="flex items-center gap-2 h-10">
<SpinnerIcon class="animate-spin" />
{{ formatMessage(messages.fetchingModpackDetails) }}
</div>
<template v-else>
<div class="flex gap-2 items-center">
<Avatar v-if="modpackProject" :src="modpackProject?.icon_url" size="40px" />
<div
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" />
<WrenchIcon v-else />
</div>
<div class="flex flex-col gap-2 justify-center">
<span class="font-semibold leading-none">
{{
modpackProject
? modpackProject.title
: formatMessage(messages.minecraftVersion, { version: instance.game_version })
}}
</span>
<span class="text-sm text-secondary leading-none">
{{
modpackProject
? modpackVersion
? modpackVersion?.version_number
: 'Unknown version'
: formatCategory(instance.loader)
}}
<template v-if="instance.loader !== 'vanilla' && !modpackProject">
{{ instance.loader_version || formatMessage(messages.unknownVersion) }}
</template>
</span>
</div>
</div>
<div class="flex gap-1">
<ButtonStyled color="orange" type="transparent" hover-color-fill="background">
<button
v-tooltip="
repairing
? formatMessage(messages.repairInProgress)
: installing || reinstalling
? formatMessage(messages.cannotWhileInstalling, {
action: formatMessage(messages.repairAction),
})
: offline
? formatMessage(messages.cannotWhileOffline, {
action: formatMessage(messages.repairAction),
})
: null
"
:disabled="installing || repairing || reinstalling || offline"
@click="repairConfirmModal.show()"
>
<SpinnerIcon v-if="repairing" class="animate-spin" />
<HammerIcon v-else />
{{
repairing
? formatMessage(messages.repairingButton)
: formatMessage(messages.repairButton)
}}
</button>
</ButtonStyled>
<ButtonStyled v-if="modpackProject" hover-color-fill="background">
<button
v-tooltip="
changingVersion
? formatMessage(messages.installingNewVersion)
: repairing
? formatMessage(messages.cannotWhileRepairing, {
action: formatMessage(messages.changeVersionAction),
})
: installing || reinstalling
? formatMessage(messages.cannotWhileInstalling, {
action: formatMessage(messages.changeVersionAction),
})
: fetching && !modpackVersions
? formatMessage(messages.changeVersionCannotWhileFetching)
: offline
? formatMessage(messages.cannotWhileOffline, {
action: formatMessage(messages.changeVersionAction),
})
: null
"
:disabled="
changingVersion ||
repairing ||
installing ||
reinstalling ||
offline ||
fetching ||
!modpackVersions
"
@click="
() => {
changingVersion = true
modpackVersionModal.show()
}
"
>
<SpinnerIcon v-if="changingVersion" class="animate-spin" />
<TransferIcon v-else />
{{
changingVersion
? formatMessage(messages.installingButton)
: formatMessage(messages.changeVersionButton)
}}
</button>
</ButtonStyled>
</div>
</template>
</div>
<template v-if="!instance.linked_data || !instance.linked_data.locked">
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.platform) }}
</h2>
<Chips v-if="loaders" v-model="loader" :items="loaders.map((x) => x.name)" class="mt-2" />
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.gameVersion) }}
</h2>
<div class="flex flex-wrap mt-2 gap-2">
<TeleportDropdownMenu
v-if="selectableGameVersionNumbers !== undefined"
v-model="gameVersion"
:options="selectableGameVersionNumbers"
name="Game Version Dropdown"
/>
<Checkbox
v-if="hasSnapshots"
v-model="showSnapshots"
:label="formatMessage(messages.showAllVersions)"
/>
</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) }) }}
</h2>
<TeleportDropdownMenu
v-if="selectableLoaderVersions"
:model-value="selectableLoaderVersions[loaderVersionIndex]"
:options="selectableLoaderVersions"
:display-name="(option: ManifestLoaderVersion) => option?.id"
name="Version selector"
class="mt-2"
@change="(value) => (loaderVersionIndex = value.index)"
/>
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
<IssuesIcon />
{{ formatMessage(messages.noLoaderVersions, { loader: loader, version: gameVersion }) }}
</div>
</template>
<div class="mt-4 flex flex-wrap gap-2">
<ButtonStyled color="brand">
<button
v-tooltip="
installing || reinstalling
? formatMessage(messages.installInProgress)
: !isChanged
? formatMessage(
loader === 'vanilla'
? messages.alreadyInstalledVanilla
: messages.alreadyInstalledModded,
{
platform: formatCategory(loader),
version: instance.loader_version,
game_version: gameVersion,
},
)
: repairing
? formatMessage(messages.cannotWhileRepairing, {
action: formatMessage(messages.installAction),
})
: offline
? formatMessage(messages.cannotWhileOffline, {
action: formatMessage(messages.installAction),
})
: null
"
:disabled="!isValid || !isChanged || editing || offline || repairing"
@click="saveGvLoaderEdits()"
>
<SpinnerIcon v-if="editing" class="animate-spin" />
<DownloadIcon v-else />
{{
editing
? formatMessage(messages.installingButton)
: formatMessage(messages.installButton)
}}
</button>
</ButtonStyled>
<ButtonStyled>
<button
:disabled="!isChanged"
@click="
() => {
loader = instance.loader
gameVersion = instance.game_version
resetLoaderVersionIndex()
}
"
>
<UndoIcon />
{{ formatMessage(messages.resetSelections) }}
</button>
</ButtonStyled>
</div>
</template>
<template v-else>
<template v-if="instance.linked_data && instance.linked_data.locked">
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.unlinkInstanceTitle) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.unlinkInstanceDescription) }}
</p>
<ButtonStyled>
<button class="mt-2" @click="modalConfirmUnpair.show()">
<UnlinkIcon /> {{ formatMessage(messages.unlinkInstanceButton) }}
</button>
</ButtonStyled>
<template v-if="modpackProject">
<div>
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast block mt-4">
{{ formatMessage(messages.reinstallModpackTitle) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.reinstallModpackDescription) }}
</p>
</div>
<ButtonStyled color="red" type="outlined">
<button
v-tooltip="
reinstalling
? formatMessage(messages.reinstallingModpackButton)
: repairing
? formatMessage(messages.cannotWhileRepairing, {
action: formatMessage(messages.reinstallAction),
})
: installing
? formatMessage(messages.cannotWhileInstalling, {
action: formatMessage(messages.reinstallAction),
})
: offline
? formatMessage(messages.cannotWhileOffline, {
action: formatMessage(messages.reinstallAction),
})
: null
"
class="mt-2"
:disabled="
changingVersion ||
repairing ||
installing ||
offline ||
fetching ||
!modpackVersions
"
@click="modalConfirmReinstall.show()"
>
<SpinnerIcon v-if="reinstalling" class="animate-spin" />
<DownloadIcon v-else />
{{
reinstalling
? formatMessage(messages.reinstallingModpackButton)
: formatMessage(messages.reinstallModpackButton)
}}
</button>
</ButtonStyled>
</template>
</template>
</template>
</div>
</template>

View File

@@ -1,188 +0,0 @@
<script setup lang="ts">
import { Checkbox, Slider } from '@modrinth/ui'
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
import { computed, readonly, ref, watch } from 'vue'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { get_max_memory } from '@/helpers/jre'
import { get } from '@/helpers/settings'
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const globalSettings = (await get().catch(handleError)) as AppSettings
const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
)
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
const envVars = ref(
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('='))
.join(' '),
)
const overrideMemorySettings = ref(!!props.instance.memory)
const memory = ref(props.instance.memory ?? globalSettings.memory)
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
const editProfileObject = computed(() => {
const editProfile: {
java_path?: string
extra_launch_args?: string[]
custom_env_vars?: string[][]
memory?: MemorySettings
} = {}
if (overrideJavaInstall.value) {
if (javaInstall.value.path !== '') {
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
}
}
if (overrideJavaArgs.value) {
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
if (overrideEnvVars.value) {
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
}
if (overrideMemorySettings.value) {
editProfile.memory = memory.value
}
return editProfile
})
watch(
[
overrideJavaInstall,
javaInstall,
overrideJavaArgs,
javaArgs,
overrideEnvVars,
envVars,
overrideMemorySettings,
memory,
],
async () => {
await edit(props.instance.path, editProfileObject.value)
},
{ deep: true },
)
const messages = defineMessages({
javaInstallation: {
id: 'instance.settings.tabs.java.java-installation',
defaultMessage: 'Java installation',
},
javaArguments: {
id: 'instance.settings.tabs.java.java-arguments',
defaultMessage: 'Java arguments',
},
javaEnvironmentVariables: {
id: 'instance.settings.tabs.java.environment-variables',
defaultMessage: 'Environment variables',
},
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
},
hooks: {
id: 'instance.settings.tabs.java.hooks',
defaultMessage: 'Hooks',
},
})
</script>
<template>
<div>
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaInstallation) }}
</h2>
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
<template v-if="!overrideJavaInstall">
<div class="flex my-2 items-center gap-2 font-semibold">
<template v-if="javaInstall">
<CheckCircleIcon class="text-brand-green h-4 w-4" />
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
</template>
<template v-else-if="optimalJava">
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
one below:</span
>
</template>
<template v-else>
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not automatically determine a Java installation to use. Please set one
below:</span
>
</template>
</div>
<div
v-if="javaInstall && !overrideJavaInstall"
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
>
{{ javaInstall.path }}
</div>
</template>
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaMemory) }}
</h2>
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
<Slider
id="max-memory"
v-model="memory.maximum"
:disabled="!overrideMemorySettings"
:min="512"
:max="maxMemory"
:step="64"
unit="MB"
/>
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaArguments) }}
</h2>
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
<input
id="java-args"
v-model="javaArgs"
autocomplete="off"
:disabled="!overrideJavaArgs"
type="text"
class="w-full"
placeholder="Enter java arguments..."
/>
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaEnvironmentVariables) }}
</h2>
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
<input
id="env-vars"
v-model="envVars"
autocomplete="off"
:disabled="!overrideEnvVars"
type="text"
class="w-full"
placeholder="Enter environmental variables..."
/>
</div>
</template>

View File

@@ -1,165 +0,0 @@
<script setup lang="ts">
import { Checkbox, Toggle } from '@modrinth/ui'
import { computed, ref, type Ref, watch } from 'vue'
import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { get } from '@/helpers/settings'
import { edit } from '@/helpers/profile'
import type { InstanceSettingsTabProps, AppSettings } from '../../../helpers/types'
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const globalSettings = (await get().catch(handleError)) as AppSettings
const overrideWindowSettings = ref(
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
)
const resolution: Ref<[number, number]> = ref(
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
)
const fullscreenSetting: Ref<boolean> = ref(
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
)
const editProfileObject = computed(() => {
const editProfile: {
force_fullscreen?: boolean
game_resolution?: [number, number]
} = {}
if (overrideWindowSettings.value) {
editProfile.force_fullscreen = fullscreenSetting.value
if (!fullscreenSetting.value) {
editProfile.game_resolution = resolution.value
}
}
return editProfile
})
watch(
[overrideWindowSettings, resolution, fullscreenSetting],
async () => {
await edit(props.instance.path, editProfileObject.value)
},
{ deep: true },
)
const messages = defineMessages({
customWindowSettings: {
id: 'instance.settings.tabs.window.custom-window-settings',
defaultMessage: 'Custom window settings',
},
fullscreen: {
id: 'instance.settings.tabs.window.fullscreen',
defaultMessage: 'Fullscreen',
},
fullscreenDescription: {
id: 'instance.settings.tabs.window.fullscreen.description',
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
},
width: {
id: 'instance.settings.tabs.window.width',
defaultMessage: 'Width',
},
widthDescription: {
id: 'instance.settings.tabs.window.width.description',
defaultMessage: 'The width of the game window when launched.',
},
enterWidth: {
id: 'instance.settings.tabs.window.width.enter',
defaultMessage: 'Enter width...',
},
height: {
id: 'instance.settings.tabs.window.height',
defaultMessage: 'Height',
},
heightDescription: {
id: 'instance.settings.tabs.window.height.description',
defaultMessage: 'The height of the game window when launched.',
},
enterHeight: {
id: 'instance.settings.tabs.window.height.enter',
defaultMessage: 'Enter height...',
},
})
</script>
<template>
<div>
<Checkbox
v-model="overrideWindowSettings"
:label="formatMessage(messages.customWindowSettings)"
@update:model-value="
(value) => {
if (!value) {
resolution = globalSettings.game_resolution
fullscreenSetting = globalSettings.force_fullscreen
}
}
"
/>
<div class="mt-2 flex items-center gap-4 justify-between">
<div>
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
{{ formatMessage(messages.fullscreen) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.fullscreenDescription) }}
</p>
</div>
<Toggle
id="fullscreen"
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
:checked="fullscreenSetting"
:disabled="!overrideWindowSettings"
@update:model-value="
(e) => {
fullscreenSetting = e
}
"
/>
</div>
<div class="mt-4 flex items-center gap-4 justify-between">
<div>
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
{{ formatMessage(messages.width) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.widthDescription) }}
</p>
</div>
<input
id="width"
v-model="resolution[0]"
autocomplete="off"
:disabled="!overrideWindowSettings || fullscreenSetting"
type="number"
:placeholder="formatMessage(messages.enterWidth)"
/>
</div>
<div class="mt-4 flex items-center gap-4 justify-between">
<div>
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
{{ formatMessage(messages.height) }}
</h2>
<p class="m-0">
{{ formatMessage(messages.heightDescription) }}
</p>
</div>
<input
id="height"
v-model="resolution[1]"
autocomplete="off"
:disabled="!overrideWindowSettings || fullscreenSetting"
type="number"
:placeholder="formatMessage(messages.enterHeight)"
/>
</div>
</div>
</template>

View File

@@ -1,161 +0,0 @@
<script setup lang="ts">
import {
ReportIcon,
ModrinthIcon,
ShieldIcon,
SettingsIcon,
GaugeIcon,
PaintBrushIcon,
GameIcon,
CoffeeIcon,
} from '@modrinth/assets'
import { TabbedModal } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import { useVIntl, defineMessage } from '@vintl/vintl'
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
import { getVersion } from '@tauri-apps/api/app'
import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/plugin-os'
import { useTheming } from '@/store/state'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get, set } from '@/helpers/settings'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const devModeCounter = ref(0)
const developerModeEnabled = defineMessage({
id: 'app.settings.developer-mode-enabled',
defaultMessage: 'Developer mode enabled.',
})
const tabs = [
{
name: defineMessage({
id: 'app.settings.tabs.appearance',
defaultMessage: 'Appearance',
}),
icon: PaintBrushIcon,
content: AppearanceSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.privacy',
defaultMessage: 'Privacy',
}),
icon: ShieldIcon,
content: PrivacySettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.java-installations',
defaultMessage: 'Java installations',
}),
icon: CoffeeIcon,
content: JavaSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.default-instance-options',
defaultMessage: 'Default instance options',
}),
icon: GameIcon,
content: DefaultInstanceSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.resource-management',
defaultMessage: 'Resource management',
}),
icon: GaugeIcon,
content: ResourceManagementSettings,
},
{
name: defineMessage({
id: 'app.settings.tabs.feature-flags',
defaultMessage: 'Feature flags',
}),
icon: ReportIcon,
content: FeatureFlagSettings,
developerOnly: true,
},
]
const modal = ref()
function show() {
modal.value.show()
}
const isOpen = computed(() => modal.value?.isOpen)
defineExpose({ show, isOpen })
const version = await getVersion()
const osPlatform = getOsPlatform()
const osVersion = getOsVersion()
const settings = ref(await get())
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
function devModeCount() {
devModeCounter.value++
if (devModeCounter.value > 5) {
themeStore.devMode = !themeStore.devMode
settings.value.developer_mode = !!themeStore.devMode
devModeCounter.value = 0
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
modal.value.setTab(0)
}
}
}
</script>
<template>
<ModalWrapper ref="modal">
<template #title>
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
<SettingsIcon /> Settings
</span>
</template>
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
<template #footer>
<div class="mt-auto text-secondary text-sm">
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
{{ formatMessage(developerModeEnabled) }}
</p>
<div class="flex items-center gap-3">
<button
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
@click="devModeCount"
>
<ModrinthIcon class="w-6 h-6" />
</button>
<div>
<p class="m-0">Modrinth App {{ version }}</p>
<p class="m-0">
<span v-if="osPlatform === 'macos'">MacOS</span>
<span v-else class="capitalize">{{ osPlatform }}</span>
{{ osVersion }}
</p>
</div>
</div>
</div>
</template>
</TabbedModal>
</ModalWrapper>
</template>

View File

@@ -1,85 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ConfirmModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const props = defineProps({
confirmationText: {
type: String,
default: '',
},
hasToType: {
type: Boolean,
default: false,
},
title: {
type: String,
default: 'No title defined',
required: true,
},
description: {
type: String,
default: 'No description defined',
required: true,
},
proceedIcon: {
type: Object,
default: undefined,
},
proceedLabel: {
type: String,
default: 'Proceed',
},
danger: {
type: Boolean,
default: true,
},
showAdOnClose: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['proceed'])
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
if (props.showAdOnClose) {
show_ads_window()
}
}
function proceed() {
emit('proceed')
}
</script>
<template>
<ConfirmModal
ref="modal"
:confirmation-text="confirmationText"
:has-to-type="hasToType"
:title="title"
:description="description"
:proceed-icon="proceedIcon"
:proceed-label="proceedLabel"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
:danger="danger"
@proceed="proceed"
/>
</template>

View File

@@ -1,98 +0,0 @@
<script setup lang="ts">
import {
ChevronRightIcon,
CoffeeIcon,
InfoIcon,
WrenchIcon,
MonitorIcon,
CodeIcon,
} from '@modrinth/assets'
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
import { ref } from 'vue'
import { defineMessage, useVIntl } from '@vintl/vintl'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
import { convertFileSrc } from '@tauri-apps/api/core'
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
import type { InstanceSettingsTabProps } from '../../../helpers/types'
const { formatMessage } = useVIntl()
const props = defineProps<InstanceSettingsTabProps>()
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
{
name: defineMessage({
id: 'instance.settings.tabs.general',
defaultMessage: 'General',
}),
icon: InfoIcon,
content: GeneralSettings,
},
{
name: defineMessage({
id: 'instance.settings.tabs.installation',
defaultMessage: 'Installation',
}),
icon: WrenchIcon,
content: InstallationSettings,
},
{
name: defineMessage({
id: 'instance.settings.tabs.window',
defaultMessage: 'Window',
}),
icon: MonitorIcon,
content: WindowSettings,
},
{
name: defineMessage({
id: 'instance.settings.tabs.java',
defaultMessage: 'Java and memory',
}),
icon: CoffeeIcon,
content: JavaSettings,
},
{
name: defineMessage({
id: 'instance.settings.tabs.hooks',
defaultMessage: 'Launch hooks',
}),
icon: CodeIcon,
content: HooksSettings,
},
]
const modal = ref()
function show() {
modal.value.show()
}
defineExpose({ show })
const titleMessage = defineMessage({
id: 'instance.settings.title',
defaultMessage: 'Settings',
})
</script>
<template>
<ModalWrapper ref="modal">
<template #title>
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
size="24px"
:tint-by="props.instance.path"
/>
{{ instance.name }} <ChevronRightIcon />
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
</span>
</template>
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
</ModalWrapper>
</template>

View File

@@ -1,57 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { NewModal as Modal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
const props = defineProps({
header: {
type: String,
default: null,
},
closable: {
type: Boolean,
default: true,
},
onHide: {
type: Function,
default() {
return () => {}
},
},
showAdOnClose: {
type: Boolean,
default: true,
},
})
const modal = ref(null)
defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
if (props.showAdOnClose) {
show_ads_window()
}
props.onHide()
}
</script>
<template>
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
<template #title>
<slot name="title" />
</template>
<slot />
</Modal>
</template>

View File

@@ -1,61 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ShareModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
const themeStore = useTheming()
defineProps({
header: {
type: String,
default: 'Share',
},
shareTitle: {
type: String,
default: 'Modrinth',
},
shareText: {
type: String,
default: null,
},
link: {
type: Boolean,
default: false,
},
openInNewTab: {
type: Boolean,
default: true,
},
})
const modal = ref(null)
defineExpose({
show: (passedContent) => {
hide_ads_window()
modal.value.show(passedContent)
},
hide: () => {
onModalHide()
modal.value.hide()
},
})
function onModalHide() {
show_ads_window()
}
</script>
<template>
<ShareModal
ref="modal"
:header="header"
:share-title="shareTitle"
:share-text="shareText"
:link="link"
:open-in-new-tab="openInNewTab"
:on-hide="onModalHide"
:noblur="!themeStore.advancedRendering"
/>
</template>

View File

@@ -1,123 +0,0 @@
<script setup lang="ts">
import { Toggle, ThemeSelector, TeleportDropdownMenu } from '@modrinth/ui'
import { useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings'
import { watch, ref } from 'vue'
import { getOS } from '@/helpers/utils'
const themeStore = useTheming()
const os = ref(await getOS())
const settings = ref(await get())
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
<ThemeSelector
:update-color-theme="
(theme) => {
themeStore.setThemeState(theme)
settings.theme = theme
}
"
:current-theme="settings.theme"
:theme-options="themeStore.themeOptions"
system-theme-color="system"
/>
<div class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
<p class="m-0 mt-1">
Enables advanced rendering such as blur effects that may cause performance issues without
hardware-accelerated rendering.
</p>
</div>
<Toggle
id="advanced-rendering"
:model-value="themeStore.advancedRendering"
:checked="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = e
settings.advanced_rendering = themeStore.advancedRendering
}
"
/>
</div>
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Native Decorations</h2>
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
</div>
<Toggle
id="native-decorations"
:model-value="settings.native_decorations"
:checked="settings.native_decorations"
@update:model-value="
(e) => {
settings.native_decorations = e
}
"
/>
</div>
<div class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
</div>
<Toggle
id="minimize-launcher"
:model-value="settings.hide_on_process_start"
:checked="settings.hide_on_process_start"
@update:model-value="
(e) => {
settings.hide_on_process_start = e
}
"
/>
</div>
<div class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
</div>
<TeleportDropdownMenu
id="opening-page"
v-model="settings.default_page"
name="Opening page dropdown"
:options="['Home', 'Library']"
/>
</div>
<div class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
<p class="m-0 mt-1">Enables the ability to toggle the sidebar.</p>
</div>
<Toggle
id="toggle-sidebar"
:model-value="settings.toggle_sidebar"
:checked="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"
/>
</div>
</template>

View File

@@ -1,181 +0,0 @@
<script setup lang="ts">
import { get, set } from '@/helpers/settings'
import { ref, watch } from 'vue'
import { get_max_memory } from '@/helpers/jre'
import { handleError } from '@/store/notifications'
import { Slider, Toggle } from '@modrinth/ui'
const fetchSettings = await get()
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
const settings = ref(fetchSettings)
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_vars = setSettings.envVars
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
if (!setSettings.hooks.pre_launch) {
setSettings.hooks.pre_launch = null
}
if (!setSettings.hooks.wrapper) {
setSettings.hooks.wrapper = null
}
if (!setSettings.hooks.post_exit) {
setSettings.hooks.post_exit = null
}
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
</script>
<template>
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
Overwrites the options.txt file to start in full screen when launched.
</p>
</div>
<Toggle
id="fullscreen"
:model-value="settings.force_fullscreen"
:checked="settings.force_fullscreen"
@update:model-value="
(e) => {
settings.force_fullscreen = e
}
"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
The width of the game window when launched.
</p>
</div>
<input
id="width"
v-model="settings.game_resolution[0]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
placeholder="Enter width..."
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
The height of the game window when launched.
</p>
</div>
<input
id="height"
v-model="settings.game_resolution[1]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
class="input"
placeholder="Enter height..."
/>
</div>
<hr class="mt-4 bg-button-border border-none h-[1px]" />
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
<Slider
id="max-memory"
v-model="settings.memory.maximum"
:min="512"
:max="maxMemory"
:step="64"
unit="MB"
/>
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
<input
id="java-args"
v-model="settings.launchArgs"
autocomplete="off"
type="text"
placeholder="Enter java arguments..."
class="w-full"
/>
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
<input
id="env-vars"
v-model="settings.envVars"
autocomplete="off"
type="text"
placeholder="Enter environmental variables..."
class="w-full"
/>
<hr class="mt-4 bg-button-border border-none h-[1px]" />
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
<input
id="pre-launch"
v-model="settings.hooks.pre_launch"
autocomplete="off"
type="text"
placeholder="Enter pre-launch command..."
class="w-full"
/>
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
Wrapper command for launching Minecraft.
</p>
<input
id="wrapper"
v-model="settings.hooks.wrapper"
autocomplete="off"
type="text"
placeholder="Enter wrapper command..."
class="w-full"
/>
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
<input
id="post-exit"
v-model="settings.hooks.post_exit"
autocomplete="off"
type="text"
placeholder="Enter post-exit command..."
class="w-full"
/>
</div>
</template>

View File

@@ -1,44 +0,0 @@
<script setup lang="ts">
import { Toggle } from '@modrinth/ui'
import { useTheming } from '@/store/state'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings'
const themeStore = useTheming()
const settings = ref(await get())
const options = ref(['project_background', 'page_path'])
function getStoreValue(key: string) {
return themeStore.featureFlags[key] ?? false
}
function setStoreValue(key: string, value: boolean) {
themeStore.featureFlags[key] = value
settings.value.feature_flags[key] = value
}
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
{{ option }}
</h2>
</div>
<Toggle
id="advanced-rendering"
:model-value="getStoreValue(option)"
:checked="getStoreValue(option)"
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
/>
</div>
</template>

View File

@@ -1,32 +0,0 @@
<script setup>
import { ref } from 'vue'
import { get_java_versions, set_java_version } from '@/helpers/jre'
import { handleError } from '@/store/notifications'
import JavaSelector from '@/components/ui/JavaSelector.vue'
const javaVersions = ref(await get_java_versions().catch(handleError))
async function updateJavaVersion(version) {
if (version?.path === '') {
version.path = undefined
}
if (version?.path) {
version.path = version.path.replace('java.exe', 'javaw.exe')
}
await set_java_version(version).catch(handleError)
}
</script>
<template>
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
Java {{ javaVersion }} location
</h2>
<JavaSelector
:id="'java-selector-' + javaVersion"
v-model="javaVersions[javaVersion]"
:version="javaVersion"
@update:model-value="updateJavaVersion"
/>
</div>
</template>

View File

@@ -1,84 +0,0 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings'
import { Toggle } from '@modrinth/ui'
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
const settings = ref(await get())
watch(
settings,
async () => {
if (settings.value.telemetry) {
optInAnalytics()
} else {
optOutAnalytics()
}
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
<p class="m-0 text-sm">
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
option, you opt out and ads will no longer be shown based on your interests.
</p>
</div>
<Toggle
id="personalized-ads"
:model-value="settings.personalized_ads"
:checked="settings.personalized_ads"
@update:model-value="
(e) => {
settings.personalized_ads = e
}
"
/>
</div>
<div class="mt-4 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
<p class="m-0 text-sm">
Modrinth collects anonymized analytics and usage data to improve our user experience and
customize your experience. By disabling this option, you opt out and your data will no
longer be collected.
</p>
</div>
<Toggle
id="opt-out-analytics"
:model-value="settings.telemetry"
:checked="settings.telemetry"
@update:model-value="
(e) => {
settings.telemetry = e
}
"
/>
</div>
<div class="mt-4 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
<p class="m-0 text-sm">
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
longer show up as a game or app you are using on your Discord profile.
</p>
<p class="m-0 mt-2 text-sm">
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
as those added by mods. (app restart required to take effect)
</p>
</div>
<Toggle
id="disable-discord-rpc"
v-model="settings.discord_rpc"
:checked="settings.discord_rpc"
/>
</div>
</template>

View File

@@ -1,117 +0,0 @@
<script setup>
import { Button, Slider } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.js'
import { purge_cache_types } from '@/helpers/cache.js'
import { handleError } from '@/store/notifications.js'
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { open } from '@tauri-apps/plugin-dialog'
const settings = ref(await get())
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
async function purgeCache() {
await purge_cache_types([
'project',
'version',
'user',
'team',
'organization',
'loader_manifest',
'minecraft_manifest',
'categories',
'report_types',
'loaders',
'game_versions',
'donation_platforms',
'file_update',
'search_results',
]).catch(handleError)
}
async function findLauncherDir() {
const newDir = await open({
multiple: false,
directory: true,
title: 'Select a new app directory',
})
if (newDir) {
settings.value.custom_dir = newDir
}
}
</script>
<template>
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
The directory where the launcher stores all of its files. Changes will be applied after
restarting the launcher.
</p>
<div class="m-1 my-2">
<div class="iconified-input w-full">
<BoxIcon />
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
<Button class="r-btn" @click="findLauncherDir">
<FolderSearchIcon />
</Button>
</div>
</div>
<div>
<ConfirmModalWrapper
ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false"
proceed-label="Purge cache"
:show-ad-on-close="false"
@proceed="purgeCache"
/>
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
app to reload data. This may slow down the app temporarily.
</p>
</div>
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
<TrashIcon />
Purge cache
</button>
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
The maximum amount of files the launcher can download at the same time. Set this to a lower
value if you have a poor internet connection. (app restart required to take effect)
</p>
<Slider
id="max-downloads"
v-model="settings.max_concurrent_downloads"
:min="1"
:max="10"
:step="1"
/>
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
value if you are frequently getting I/O errors. (app restart required to take effect)
</p>
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
</template>

View File

@@ -1,20 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
import cssContent from '@/assets/stylesheets/macFix.css?inline'
export async function useCheckDisableMouseover() {
try {
// Fetch the CSS content from the Rust backend
let should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
if (should_disable_mouseover) {
// Create a style element and set its content
const styleElement = document.createElement('style')
styleElement.innerHTML = cssContent
// Append the style element to the document's head
document.head.appendChild(styleElement)
}
} catch (error) {
console.error('Error checking OS version from Rust backend', error)
}
}

View File

@@ -1,21 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
export async function init_ads_window(overrideShown = false) {
return await invoke('plugin:ads|init_ads_window', { overrideShown, dpr: window.devicePixelRatio })
}
export async function show_ads_window() {
return await invoke('plugin:ads|show_ads_window', { dpr: window.devicePixelRatio })
}
export async function hide_ads_window(reset) {
return await invoke('plugin:ads|hide_ads_window', { reset })
}
export async function record_ads_click() {
return await invoke('plugin:ads|record_ads_click')
}
export async function open_ads_link(path, origin) {
return await invoke('plugin:ads|open_link', { path, origin })
}

View File

@@ -1,23 +0,0 @@
import { posthog } from 'posthog-js'
export const initAnalytics = () => {
posthog.init('phc_hm2ihMpTAoE86xIm7XzsCB8RPiTRKivViK5biiHedm', {
persistence: 'localStorage',
})
}
export const debugAnalytics = () => {
posthog.debug()
}
export const optOutAnalytics = () => {
posthog.opt_out_capturing()
}
export const optInAnalytics = () => {
posthog.opt_in_capturing()
}
export const trackEvent = (eventName, properties) => {
posthog.capture(eventName, properties)
}

View File

@@ -1,58 +0,0 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
// Example function:
// User goes to auth_url to complete flow, and when completed, authenticate_await_completion() returns the credentials
// export async function authenticate() {
// const auth_url = await authenticate_begin_flow()
// console.log(auth_url)
// await authenticate_await_completion()
// }
/**
* Authenticate a user with Hydra - part 1.
* This begins the authentication flow quasi-synchronously.
*
* @returns {Promise<DeviceLoginSuccess>} A DeviceLoginSuccess object with two relevant fields:
* @property {string} verification_uri - The URL to go to complete the flow.
* @property {string} user_code - The code to enter on the verification_uri page.
*/
export async function login() {
return await invoke('plugin:auth|login')
}
/**
* Retrieves the default user
* @return {Promise<UUID | undefined>}
*/
export async function get_default_user() {
return await invoke('plugin:auth|get_default_user')
}
/**
* Updates the default user
* @param {UUID} user
*/
export async function set_default_user(user) {
return await invoke('plugin:auth|set_default_user', { user })
}
/**
* Remove a user account from the database
* @param {UUID} user
*/
export async function remove_user(user) {
return await invoke('plugin:auth|remove_user', { user })
}
/**
* Returns a list of users
* @returns {Promise<Credential[]>}
*/
export async function users() {
return await invoke('plugin:auth|get_users')
}

View File

@@ -1,53 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
export async function get_project(id, cacheBehaviour) {
return await invoke('plugin:cache|get_project', { id, cacheBehaviour })
}
export async function get_project_many(ids, cacheBehaviour) {
return await invoke('plugin:cache|get_project_many', { ids, cacheBehaviour })
}
export async function get_version(id, cacheBehaviour) {
return await invoke('plugin:cache|get_version', { id, cacheBehaviour })
}
export async function get_version_many(ids, cacheBehaviour) {
return await invoke('plugin:cache|get_version_many', { ids, cacheBehaviour })
}
export async function get_user(id, cacheBehaviour) {
return await invoke('plugin:cache|get_user', { id, cacheBehaviour })
}
export async function get_user_many(ids, cacheBehaviour) {
return await invoke('plugin:cache|get_user_many', { ids, cacheBehaviour })
}
export async function get_team(id, cacheBehaviour) {
return await invoke('plugin:cache|get_team', { id, cacheBehaviour })
}
export async function get_team_many(ids, cacheBehaviour) {
return await invoke('plugin:cache|get_team_many', { ids, cacheBehaviour })
}
export async function get_organization(id, cacheBehaviour) {
return await invoke('plugin:cache|get_organization', { id, cacheBehaviour })
}
export async function get_organization_many(ids, cacheBehaviour) {
return await invoke('plugin:cache|get_organization_many', { ids, cacheBehaviour })
}
export async function get_search_results(id, cacheBehaviour) {
return await invoke('plugin:cache|get_search_results', { id, cacheBehaviour })
}
export async function get_search_results_many(ids, cacheBehaviour) {
return await invoke('plugin:cache|get_search_results_many', { ids, cacheBehaviour })
}
export async function purge_cache_types(cacheTypes) {
return await invoke('plugin:cache|purge_cache_types', { cacheTypes })
}

View File

@@ -1,17 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
export async function friends() {
return await invoke('plugin:friends|friends')
}
export async function friend_statuses() {
return await invoke('plugin:friends|friend_statuses')
}
export async function add_friend(userId) {
return await invoke('plugin:friends|add_friend', { userId })
}
export async function remove_friend(userId) {
return await invoke('plugin:friends|remove_friend', { userId })
}

View File

@@ -1,51 +0,0 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
/*
JavaVersion {
path: Path
version: String
}
*/
export async function get_java_versions() {
return await invoke('plugin:jre|get_java_versions')
}
export async function set_java_version(javaVersion) {
return await invoke('plugin:jre|set_java_version', { javaVersion })
}
// Finds all the installation of Java 7, if it exists
// Returns [JavaVersion]
export async function find_filtered_jres(version) {
return await invoke('plugin:jre|jre_find_filtered_jres', { version })
}
// Gets java version from a specific path by trying to run 'java -version' on it.
// This also validates it, as it returns null if no valid java version is found at the path
export async function get_jre(path) {
return await invoke('plugin:jre|jre_get_jre', { path })
}
// Tests JRE version by running 'java -version' on it.
// Returns true if the version is valid, and matches given (after extraction)
export async function test_jre(path, majorVersion, minorVersion) {
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
}
// Automatically installs specified java version
export async function auto_install_java(javaVersion) {
return await invoke('plugin:jre|jre_auto_install_java', { javaVersion })
}
// Get max memory in KiB
export async function get_max_memory() {
return await invoke('plugin:jre|jre_get_max_memory')
}

View File

@@ -1,13 +0,0 @@
import { invoke } from '@tauri-apps/api/core'
/// Gets the game versions from daedalus
// Returns a VersionManifest
export async function get_game_versions() {
return await invoke('plugin:metadata|metadata_get_game_versions')
}
// Gets the given loader versions from daedalus
// Returns Manifest
export async function get_loader_versions(loader) {
return await invoke('plugin:metadata|metadata_get_loader_versions', { loader })
}

View File

@@ -1,18 +0,0 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
export async function login() {
return await invoke('plugin:mr-auth|modrinth_login')
}
export async function logout() {
return await invoke('plugin:mr-auth|logout')
}
export async function get() {
return await invoke('plugin:mr-auth|get')
}

View File

@@ -1,23 +0,0 @@
/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
/// Gets all running process IDs with a given profile path
/// Returns [u32]
export async function get_by_profile_path(path) {
return await invoke('plugin:process|process_get_by_profile_path', { path })
}
/// Gets all running process IDs with a given profile path
/// Returns [u32]
export async function get_all() {
return await invoke('plugin:process|process_get_all')
}
/// Kills a process by UUID
export async function kill(uuid) {
return await invoke('plugin:process|process_kill', { uuid })
}

View File

@@ -1,115 +0,0 @@
import type { ModrinthId } from '@modrinth/utils'
type GameInstance = {
path: string
install_stage: InstallStage
name: string
icon_path?: string
game_version: string
loader: InstanceLoader
loader_version?: string
groups: string[]
linked_data?: LinkedData
created: Date
modified: Date
last_played?: Date
submitted_time_played: number
recent_time_played: number
java_path?: string
extra_launch_args?: string[]
custom_env_vars?: [string, string][]
memory?: MemorySettings
force_fullscreen?: boolean
game_resolution?: [number, number]
hooks: Hooks
}
type InstallStage =
| 'installed'
| 'minecraft_installing'
| 'pack_installed'
| 'pack_installing'
| 'not_installed'
type LinkedData = {
project_id: ModrinthId
version_id: ModrinthId
locked: boolean
}
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type MemorySettings = {
maximum: number
}
type WindowSize = {
width: number
height: number
}
type Hooks = {
pre_launch?: string
wrapper?: string
post_exit?: string
}
type Manifest = {
gameVersions: ManifestGameVersion[]
}
type ManifestGameVersion = {
id: string
stable: boolean
loaders: ManifestLoaderVersion[]
}
type ManifestLoaderVersion = {
id: string
url: string
stable: boolean
}
type AppSettings = {
max_concurrent_downloads: number
max_concurrent_writes: number
theme: 'dark' | 'light' | 'oled'
default_page: 'Home' | 'Library'
collapsed_navigation: boolean
advanced_rendering: boolean
native_decorations: boolean
telemetry: boolean
discord_rpc: boolean
developer_mode: boolean
personalized_ads: boolean
onboarded: boolean
extra_launch_args: string[]
custom_env_vars: [string, string][]
memory: MemorySettings
force_fullscreen: boolean
game_resolution: [number, number]
hide_on_process_start: boolean
hooks: Hooks
custom_dir?: string
prev_custom_dir?: string
migrated: boolean
}
export type InstanceSettingsTabProps = {
instance: GameInstance
offline?: boolean
}

View File

@@ -1,51 +0,0 @@
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
import { invoke } from '@tauri-apps/api/core'
export async function isDev() {
return await invoke('is_dev')
}
// One of 'Windows', 'Linux', 'MacOS'
export async function getOS() {
return await invoke('plugin:utils|get_os')
}
export async function openPath(path) {
return await invoke('plugin:utils|open_path', { path })
}
export async function highlightInFolder(path) {
return await invoke('plugin:utils|highlight_in_folder', { path })
}
export async function showLauncherLogsFolder() {
return await invoke('plugin:utils|show_launcher_logs_folder', {})
}
// Opens a profile's folder in the OS file explorer
export async function showProfileInFolder(path) {
const fullPath = await get_full_path(path)
return await openPath(fullPath)
}
export async function highlightModInProfile(profilePath, projectPath) {
const fullPath = await get_mod_full_path(profilePath, projectPath)
return await highlightInFolder(fullPath)
}
export async function restartApp() {
return await invoke('restart_app')
}
export const releaseColor = (releaseType) => {
switch (releaseType) {
case 'release':
return 'green'
case 'beta':
return 'orange'
case 'alpha':
return 'red'
default:
return ''
}
}

View File

@@ -1,323 +0,0 @@
{
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
"app.settings.tabs.appearance": {
"message": "Appearance"
},
"app.settings.tabs.default-instance-options": {
"message": "Default instance options"
},
"app.settings.tabs.feature-flags": {
"message": "Feature flags"
},
"app.settings.tabs.java-installations": {
"message": "Java installations"
},
"app.settings.tabs.privacy": {
"message": "Privacy"
},
"app.settings.tabs.resource-management": {
"message": "Resource management"
},
"instance.filter.disabled": {
"message": "Disabled projects"
},
"instance.filter.updates-available": {
"message": "Updates available"
},
"instance.settings.tabs.general": {
"message": "General"
},
"instance.settings.tabs.general.delete": {
"message": "Delete instance"
},
"instance.settings.tabs.general.delete.button": {
"message": "Delete instance"
},
"instance.settings.tabs.general.delete.description": {
"message": "Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Deleting..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Duplicate"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Cannot duplicate while installing."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Duplicate instance"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Creates a copy of this instance, including worlds, configs, mods, etc."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Edit icon"
},
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Remove icon"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Replace icon"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Select icon"
},
"instance.settings.tabs.general.library-groups": {
"message": "Library groups"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Create new group"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Library groups allow you to organize your instances into different sections in your library."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Enter group name"
},
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.hooks": {
"message": "Launch hooks"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Custom launch hooks"
},
"instance.settings.tabs.hooks.description": {
"message": "Hooks allow advanced users to run certain system commands before and after launching the game."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Post-exit"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Ran after the game closes."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Enter post-exit command..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Pre-launch"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Ran before the instance is launched."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Enter pre-launch command..."
},
"instance.settings.tabs.hooks.title": {
"message": "Game launch hooks"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Wrapper command for launching Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Enter wrapper command..."
},
"instance.settings.tabs.installation": {
"message": "Installation"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "{platform} {version} for Minecraft {game_version} already installed"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Vanilla {game_version} already installed"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "Change version"
},
"instance.settings.tabs.installation.change-version.button.install": {
"message": "Install"
},
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Installing"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Fetching modpack versions"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Installing new version"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Currently installed"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Debug information:"
},
"instance.settings.tabs.installation.fetching-modpack-details": {
"message": "Fetching modpack details"
},
"instance.settings.tabs.installation.game-version": {
"message": "Game version"
},
"instance.settings.tabs.installation.install": {
"message": "Install"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Installation in progress"
},
"instance.settings.tabs.installation.loader-version": {
"message": "{loader} version"
},
"instance.settings.tabs.installation.minecraft-version": {
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-connection": {
"message": "Cannot fetch linked modpack details. Please check your internet connection."
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "{loader} is not available for Minecraft {version}. Try another mod loader."
},
"instance.settings.tabs.installation.no-modpack-found": {
"message": "This instance is linked to a modpack, but the modpack could not be found on Modrinth."
},
"instance.settings.tabs.installation.platform": {
"message": "Platform"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "Reinstall modpack"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Reinstalling modpack"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Reinstalling will reset all installed or modified content to what is provided by the modpack, removing any mods or content you have added on top of the original installation. This may fix unexpected behavior if changes have been made to the instance, but if your worlds now depend on additional installed content, it may break existing worlds."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Are you sure you want to reinstall this instance?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Resets the instance's content to its original state, removing any mods or content you have added on top of the original modpack."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Reinstall modpack"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Repair"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Repairing"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Repair instance?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Repair in progress"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Reset to current"
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Show all versions"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "change version"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "install"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "reinstall"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "repair"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Cannot {action} while installing"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Cannot {action} while offline"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Cannot {action} while repairing"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(unknown version)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "Unlink instance"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal."
},
"instance.settings.tabs.installation.unlink.confirm.title": {
"message": "Are you sure you want to unlink this instance?"
},
"instance.settings.tabs.installation.unlink.description": {
"message": "This instance is linked to a modpack, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the modpack."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Unlink from modpack"
},
"instance.settings.tabs.java": {
"message": "Java and memory"
},
"instance.settings.tabs.java.environment-variables": {
"message": "Environment variables"
},
"instance.settings.tabs.java.hooks": {
"message": "Hooks"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Java arguments"
},
"instance.settings.tabs.java.java-installation": {
"message": "Java installation"
},
"instance.settings.tabs.java.java-memory": {
"message": "Memory allocated"
},
"instance.settings.tabs.window": {
"message": "Window"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Custom window settings"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Fullscreen"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "Make the game start in full screen when launched (using options.txt)."
},
"instance.settings.tabs.window.height": {
"message": "Height"
},
"instance.settings.tabs.window.height.description": {
"message": "The height of the game window when launched."
},
"instance.settings.tabs.window.height.enter": {
"message": "Enter height..."
},
"instance.settings.tabs.window.width": {
"message": "Width"
},
"instance.settings.tabs.window.width.description": {
"message": "The width of the game window when launched."
},
"instance.settings.tabs.window.width.enter": {
"message": "Enter width..."
},
"instance.settings.title": {
"message": "Settings"
},
"search.filter.locked.instance": {
"message": "Provided by the instance"
},
"search.filter.locked.instance-game-version.title": {
"message": "Game version is provided by the instance"
},
"search.filter.locked.instance-loader.title": {
"message": "Loader is provided by the instance"
},
"search.filter.locked.instance.sync": {
"message": "Sync with instance"
}
}

View File

@@ -1,52 +0,0 @@
import { createApp } from 'vue'
import router from '@/routes'
import App from '@/App.vue'
import { createPinia } from 'pinia'
import FloatingVue from 'floating-vue'
import 'floating-vue/dist/style.css'
import { createPlugin } from '@vintl/vintl/plugin'
import * as Sentry from '@sentry/vue'
const VIntlPlugin = createPlugin({
controllerOpts: {
defaultLocale: 'en-US',
locale: 'en-US',
locales: [
{
tag: 'en-US',
meta: {
displayName: 'American English',
},
},
],
},
globalMixin: true,
injectInto: [],
})
const pinia = createPinia()
let app = createApp(App)
Sentry.init({
app,
dsn: 'https://9508775ee5034536bc70433f5f531dd4@o485889.ingest.us.sentry.io/4504579615227904',
integrations: [Sentry.browserTracingIntegration({ router })],
tracesSampleRate: 0.1,
})
app.use(router)
app.use(pinia)
app.use(FloatingVue, {
themes: {
'ribbit-popout': {
$extend: 'dropdown',
placement: 'bottom-end',
instantMove: true,
distance: 8,
},
},
})
app.use(VIntlPlugin)
app.mount('#app')

View File

@@ -1,523 +0,0 @@
<script setup lang="ts">
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import type { Ref } from 'vue'
import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets'
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
import {
SearchFilterControl,
SearchSidebarFilter,
Button,
Checkbox,
DropdownSelect,
LoadingIndicator,
Pagination,
useSearch,
} from '@modrinth/ui'
import { handleError } from '@/store/state'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import type { LocationQuery } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { get_search_results } from '@/helpers/cache.js'
import NavTabs from '@/components/ui/NavTabs.vue'
import type Instance from '@/components/ui/Instance.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import { defineMessages, useVIntl } from '@vintl/vintl'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
const { formatMessage } = useVIntl()
const router = useRouter()
const route = useRoute()
const projectTypes = computed(() => {
return [route.params.projectType as ProjectType]
})
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories().catch(handleError).then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
])
const tags: Ref<Tags> = computed(() => ({
gameVersions: availableGameVersions.value as GameVersion[],
loaders: loaders.value as Platform[],
categories: categories.value as Category[],
}))
type Instance = {
game_version: string
loader: string
path: string
install_stage: string
icon_path?: string
name: string
}
type InstanceProject = {
metadata: {
project_id: string
}
}
const instance: Ref<Instance | null> = ref(null)
const instanceProjects: Ref<InstanceProject[] | null> = ref(null)
const instanceHideInstalled = ref(false)
const newlyInstalled = ref([])
const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
await updateInstanceContext()
watch(route, () => {
updateInstanceContext()
})
async function updateInstanceContext() {
if (route.query.i) {
;[instance.value, instanceProjects.value] = await Promise.all([
getInstance(route.query.i).catch(handleError),
getInstanceProjects(route.query.i).catch(handleError),
])
newlyInstalled.value = []
}
if (route.query.ai && !(projectTypes.value.length === 1 && projectTypes.value[0] === 'modpack')) {
instanceHideInstalled.value = route.query.ai === 'true'
}
if (instance.value && instance.value.path !== route.query.i && route.path.startsWith('/browse')) {
instance.value = null
instanceHideInstalled.value = false
}
}
const instanceFilters = computed(() => {
const filters = []
if (instance.value) {
const gameVersion = instance.value.game_version
if (gameVersion) {
filters.push({
type: 'game_version',
option: gameVersion,
})
}
const platform = instance.value.loader
const supportedModLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
if (platform && projectTypes.value.includes('mod') && supportedModLoaders.includes(platform)) {
filters.push({
type: 'mod_loader',
option: platform,
})
}
if (instanceHideInstalled.value && instanceProjects.value) {
const installedMods = Object.values(instanceProjects.value)
.filter((x) => x.metadata)
.map((x) => x.metadata.project_id)
installedMods.push(...newlyInstalled.value)
installedMods
?.map((x) => ({
type: 'project_id',
option: `project_id:${x}`,
negative: true,
}))
.forEach((x) => filters.push(x))
}
}
return filters
})
const {
// Selections
query,
currentSortType,
currentFilters,
toggledGroups,
maxResults,
currentPage,
overriddenProvidedFilterTypes,
// Lists
filters,
sortTypes,
// Computed
requestParams,
// Functions
createPageParams,
} = useSearch(projectTypes, tags, instanceFilters)
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setContext({ name: 'Discover content', link: route.path, query: route.query })
const loading = ref(true)
const projectType = ref(route.params.projectType)
watch(projectType, () => {
loading.value = true
})
type SearchResult = {
project_id: string
}
type SearchResults = {
total_hits: number
limit: number
hits: SearchResult[]
}
const results: Ref<SearchResults | null> = shallowRef(null)
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
)
watch(requestParams, () => {
if (!route.params.projectType) return
refreshSearch()
})
async function refreshSearch() {
let rawResults = await get_search_results(requestParams.value)
if (!rawResults) {
rawResults = {
result: {
hits: [],
total_hits: 0,
limit: 1,
},
}
}
if (instance.value) {
for (const val of rawResults.result.hits) {
val.installed =
newlyInstalled.value.includes(val.project_id) ||
Object.values(instanceProjects.value).some(
(x) => x.metadata && x.metadata.project_id === val.project_id,
)
}
}
results.value = rawResults.result
const persistentParams: LocationQuery = {}
for (const [key, value] of Object.entries(route.query)) {
if (PERSISTENT_QUERY_PARAMS.includes(key)) {
persistentParams[key] = value
}
}
if (instanceHideInstalled.value) {
persistentParams.ai = 'true'
} else {
delete persistentParams.ai
}
const params = {
...persistentParams,
...createPageParams(),
}
breadcrumbs.setContext({
name: 'Discover content',
link: `/browse/${projectType.value}`,
query: params,
})
await router.replace({ path: route.path, query: params })
loading.value = false
}
async function setPage(newPageNumber: number) {
currentPage.value = newPageNumber
await onSearchChangeToTop()
}
const searchWrapper: Ref<HTMLElement | null> = ref(null)
async function onSearchChangeToTop() {
await nextTick()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function clearSearch() {
query.value = ''
}
watch(
() => route.params.projectType,
async (newType) => {
// Check if the newType is not the same as the current value
if (!newType || newType === projectType.value) return
projectType.value = newType
currentSortType.value = { display: 'Relevance', name: 'relevance' }
query.value = ''
},
)
const selectableProjectTypes = computed(() => {
let dataPacks = false,
mods = false,
modpacks = false
if (instance.value) {
if (
availableGameVersions.value.findIndex((x) => x.version === instance.value.game_version) <=
availableGameVersions.value.findIndex((x) => x.version === '1.13')
) {
dataPacks = true
}
if (instance.value.loader !== 'vanilla') {
mods = true
}
} else {
dataPacks = true
mods = true
modpacks = true
}
const params: LocationQuery = {}
if (route.query.i) {
params.i = route.query.i
}
if (route.query.ai) {
params.ai = route.query.ai
}
const links = [
{ label: 'Modpacks', href: `/browse/modpack`, shown: modpacks },
{ label: 'Mods', href: `/browse/mod`, shown: mods },
{ label: 'Resource Packs', href: `/browse/resourcepack` },
{ label: 'Data Packs', href: `/browse/datapack`, shown: dataPacks },
{ label: 'Shaders', href: `/browse/shader` },
]
if (params) {
return links.map((link) => {
return {
...link,
href: {
path: link.href,
query: params,
},
}
})
}
return links
})
const messages = defineMessages({
gameVersionProvidedByInstance: {
id: 'search.filter.locked.instance-game-version.title',
defaultMessage: 'Game version is provided by the instance',
},
modLoaderProvidedByInstance: {
id: 'search.filter.locked.instance-loader.title',
defaultMessage: 'Loader is provided by the instance',
},
providedByInstance: {
id: 'search.filter.locked.instance',
defaultMessage: 'Provided by the instance',
},
syncFilterButton: {
id: 'search.filter.locked.instance.sync',
defaultMessage: 'Sync with instance',
},
})
const options = ref(null)
const handleRightClick = (event, result) => {
options.value.showMenu(event, result, [
{
name: 'open_link',
},
{
name: 'copy_link',
},
])
}
const handleOptionsClick = (args) => {
switch (args.option) {
case 'open_link':
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
break
case 'copy_link':
navigator.clipboard.writeText(
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
)
break
}
}
await refreshSearch()
</script>
<template>
<Teleport v-if="filters" to="#sidebar-teleport-target">
<div
v-if="instance"
class="border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
>
<Checkbox
v-model="instanceHideInstalled"
label="Hide installed content"
class="filter-checkbox"
@update:model-value="onSearchChangeToTop()"
@click.prevent.stop
/>
</div>
<SearchSidebarFilter
v-for="filter in filters.filter((f) => f.display !== 'none')"
:key="`filter-${filter.id}`"
v-model:selected-filters="currentFilters"
v-model:toggled-groups="toggledGroups"
v-model:overridden-provided-filter-types="overriddenProvidedFilterTypes"
:provided-filters="instanceFilters"
:filter-type="filter"
class="border-0 border-b-[1px] [&:first-child>button]:pt-4 last:border-b-0 border-[--brand-gradient-border] border-solid"
button-class="button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg"
content-class="mb-3"
inner-panel-class="ml-2 mr-3"
:open-by-default="
filter.id.startsWith('category') || filter.id === 'environment' || filter.id === 'license'
"
>
<template #header>
<h3 class="text-base m-0">{{ filter.formatted_name }}</h3>
</template>
<template #locked-game_version>
{{ formatMessage(messages.gameVersionProvidedByInstance) }}
</template>
<template #locked-mod_loader>
{{ formatMessage(messages.modLoaderProvidedByInstance) }}
</template>
<template #sync-button> {{ formatMessage(messages.syncFilterButton) }} </template>
</SearchSidebarFilter>
</Teleport>
<div ref="searchWrapper" class="flex flex-col gap-3 p-6">
<template v-if="instance">
<InstanceIndicator :instance="instance" />
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
</template>
<NavTabs :links="selectableProjectTypes" />
<div class="iconified-input">
<SearchIcon aria-hidden="true" class="text-lg" />
<input
v-model="query"
class="h-12 card-shadow"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="`Search ${projectType}s...`"
/>
<Button v-if="query" class="r-btn" @click="() => clearSearch()">
<XIcon />
</Button>
</div>
<div class="flex gap-2">
<DropdownSelect
v-slot="{ selected }"
v-model="currentSortType"
class="max-w-[16rem]"
name="Sort by"
:options="sortTypes as any"
:display-name="(option: SortType | undefined) => option?.display"
>
<span class="font-semibold text-primary">Sort by: </span>
<span class="font-semibold text-secondary">{{ selected }}</span>
</DropdownSelect>
<DropdownSelect
v-slot="{ selected }"
v-model="maxResults"
name="Max results"
:options="[5, 10, 15, 20, 50, 100]"
class="max-w-[9rem]"
>
<span class="font-semibold text-primary">View: </span>
<span class="font-semibold text-secondary">{{ selected }}</span>
</DropdownSelect>
<Pagination :page="currentPage" :count="pageCount" class="ml-auto" @switch-page="setPage" />
</div>
<SearchFilterControl
v-model:selected-filters="currentFilters"
:filters="filters.filter((f) => f.display !== 'none')"
:provided-filters="instanceFilters"
:overridden-provided-filter-types="overriddenProvidedFilterTypes"
:provided-message="messages.providedByInstance"
/>
<div class="search">
<section v-if="loading" class="offline">
<LoadingIndicator />
</section>
<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">
<SearchCard
v-for="result in results.hits"
:key="result?.project_id"
:project="result"
:instance="instance"
:categories="[
...categories.filter(
(cat) =>
result?.display_categories.includes(cat.name) && cat.project_type === projectType,
),
...loaders.filter(
(loader) =>
result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes(projectType),
),
]"
:installed="result.installed || newlyInstalled.includes(result.project_id)"
@install="
(id) => {
newlyInstalled.push(id)
}
"
@contextmenu.prevent.stop="(event) => handleRightClick(event, result)"
/>
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
</ContextMenu>
</section>
<div class="flex justify-end">
<pagination
:page="currentPage"
:count="pageCount"
class="pagination-after"
@switch-page="setPage"
/>
</div>
</div>
</div>
</template>

View File

@@ -1,4 +0,0 @@
import Index from './Index.vue'
import Browse from './Browse.vue'
export { Index, Browse }

View File

@@ -1,622 +0,0 @@
<template>
<div
class="p-6 pr-2 pb-4"
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
>
<ExportModal ref="exportModal" :instance="instance" />
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
<ContentPageHeader>
<template #icon>
<Avatar :src="icon" :alt="instance.name" size="96px" :tint-by="instance.path" />
</template>
<template #title>
{{ instance.name }}
</template>
<template #summary> </template>
<template #stats>
<div
class="flex items-center gap-2 font-semibold transform capitalize border-0 border-solid border-divider pr-4 md:border-r"
>
<GameIcon class="h-6 w-6 text-secondary" />
{{ instance.loader }} {{ instance.game_version }}
</div>
<div class="flex items-center gap-2 font-semibold">
<TimerIcon class="h-6 w-6 text-secondary" />
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>
<template v-else> Never played </template>
</div>
</template>
<template #actions>
<div class="flex gap-2">
<ButtonStyled
v-if="instance.install_stage.includes('installing')"
color="brand"
size="large"
>
<button disabled>Installing...</button>
</ButtonStyled>
<ButtonStyled
v-else-if="instance.install_stage !== 'installed'"
color="brand"
size="large"
>
<button @click="repairInstance()">
<DownloadIcon />
Repair
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing === true" color="red" size="large">
<button @click="stopInstance('InstancePage')">
<StopCircleIcon />
Stop
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="playing === false && loading === false"
color="brand"
size="large"
>
<button @click="startInstance('InstancePage')">
<PlayIcon />
Play
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="loading === true && playing === false"
color="brand"
size="large"
>
<button disabled>Loading...</button>
</ButtonStyled>
<ButtonStyled size="large" circular>
<button v-tooltip="'Instance settings'" @click="settingsModal.show()">
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled size="large" type="transparent" circular>
<OverflowMenu
:options="[
{
id: 'open-folder',
action: () => showProfileInFolder(instance.path),
},
{
id: 'export-mrpack',
action: () => $refs.exportModal.show(),
},
]"
>
<MoreVerticalIcon />
<template #share-instance> <UserPlusIcon /> Share instance </template>
<template #host-a-server> <ServerIcon /> Create a server </template>
<template #open-folder> <FolderOpenIcon /> Open folder </template>
<template #export-mrpack> <PackageIcon /> Export modpack </template>
</OverflowMenu>
</ButtonStyled>
</div>
</template>
</ContentPageHeader>
</div>
<div class="px-6">
<NavTabs :links="tabs" />
</div>
<div class="p-6 pt-4">
<RouterView v-slot="{ Component }" :key="instance.path">
<template v-if="Component">
<Suspense
:key="instance.path"
@pending="loadingBar.startLoading()"
@resolve="loadingBar.stopLoading()"
>
<component
:is="Component"
:instance="instance"
:options="options"
:offline="offline"
:playing="playing"
:versions="modrinthVersions"
:installed="instance.install_stage !== 'installed'"
></component>
<template #fallback>
<LoadingIndicator />
</template>
</Suspense>
</template>
</RouterView>
</div>
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #play> <PlayIcon /> Play </template>
<template #stop> <StopCircleIcon /> Stop </template>
<template #add_content> <PlusIcon /> Add content </template>
<template #edit> <EditIcon /> Edit </template>
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
<template #open_folder> <ClipboardCopyIcon /> Open folder </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
<template #open_link> <ClipboardCopyIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_names><EditIcon />Copy names</template>
<template #copy_slugs><HashIcon />Copy slugs</template>
<template #copy_links><GlobeIcon />Copy links</template>
<template #toggle><EditIcon />Toggle selected</template>
<template #disable><XIcon />Disable selected</template>
<template #enable><CheckCircleIcon />Enable selected</template>
<template #hide_show><EyeIcon />Show/Hide unselected</template>
<template #update_all
><UpdatedIcon />Update {{ selected.length > 0 ? 'selected' : 'all' }}</template
>
<template #filter_update><UpdatedIcon />Select Updatable</template>
</ContextMenu>
</template>
<script setup>
import {
Avatar,
ButtonStyled,
ContentPageHeader,
LoadingIndicator,
OverflowMenu,
} from '@modrinth/ui'
import {
CheckCircleIcon,
ClipboardCopyIcon,
DownloadIcon,
EditIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
GameIcon,
GlobeIcon,
HashIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
PlusIcon,
ServerIcon,
SettingsIcon,
StopCircleIcon,
TimerIcon,
UpdatedIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
import { process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router'
import { computed, onUnmounted, ref, watch } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { trackEvent } from '@/helpers/analytics'
import { convertFileSrc } from '@tauri-apps/api/core'
import { handleSevereError } from '@/store/error.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import ExportModal from '@/components/ui/ExportModal.vue'
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
dayjs.extend(duration)
dayjs.extend(relativeTime)
const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const instance = ref()
const modrinthVersions = ref([])
const playing = ref(false)
const loading = ref(false)
async function fetchInstance() {
instance.value = await get(route.params.id).catch(handleError)
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
get_project(instance.value.linked_data.project_id, 'must_revalidate')
.catch(handleError)
.then((project) => {
if (project && project.versions) {
get_version_many(project.versions, 'must_revalidate')
.catch(handleError)
.then((versions) => {
modrinthVersions.value = versions.sort(
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
)
})
}
})
}
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
playing.value = runningProcesses.length > 0
}
await fetchInstance()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/instance')) {
await fetchInstance()
}
},
)
const tabs = computed(() => [
{
label: 'Content',
href: `/instance/${encodeURIComponent(route.params.id)}`,
},
{
label: 'Logs',
href: `/instance/${encodeURIComponent(route.params.id)}/logs`,
},
])
breadcrumbs.setName(
'Instance',
instance.value.name.length > 40
? instance.value.name.substring(0, 40) + '...'
: instance.value.name,
)
breadcrumbs.setContext({
name: instance.value.name,
link: route.path,
query: route.query,
})
const loadingBar = useLoading()
const options = ref(null)
const startInstance = async (context) => {
loading.value = true
try {
await run(route.params.id)
playing.value = true
} catch (err) {
handleSevereError(err, { profilePath: route.params.id })
}
loading.value = false
trackEvent('InstanceStart', {
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
})
}
const stopInstance = async (context) => {
playing.value = false
await kill(route.params.id).catch(handleError)
trackEvent('InstanceStop', {
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
})
}
const repairInstance = async () => {
await finish_install(instance.value)
}
const handleRightClick = (event) => {
const baseOptions = [
{ name: 'add_content' },
{ type: 'divider' },
{ name: 'edit' },
{ name: 'open_folder' },
{ name: 'copy_path' },
]
options.value.showMenu(
event,
instance.value,
playing.value
? [
{
name: 'stop',
color: 'danger',
},
...baseOptions,
]
: [
{
name: 'play',
color: 'primary',
},
...baseOptions,
],
)
}
const handleOptionsClick = async (args) => {
switch (args.option) {
case 'play':
await startInstance('InstancePageContextMenu')
break
case 'stop':
await stopInstance('InstancePageContextMenu')
break
case 'add_content':
await router.push({
path: `/browse/${instance.value.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: route.params.id },
})
break
case 'edit':
await router.push({
path: `/instance/${encodeURIComponent(route.params.id)}/options`,
})
break
case 'open_folder':
await showProfileInFolder(instance.value.path)
break
case 'copy_path': {
const fullPath = await get_full_path(instance.value.path)
await navigator.clipboard.writeText(fullPath)
break
}
}
}
const unlistenProfiles = await profile_listener(async (event) => {
if (event.profile_path_id === route.params.id) {
if (event.event === 'removed') {
await router.push({
path: '/',
})
return
}
instance.value = await get(route.params.id).catch(handleError)
}
})
const unlistenProcesses = await process_listener((e) => {
if (e.event === 'finished' && e.profile_path_id === route.params.id) {
playing.value = false
}
})
const icon = computed(() =>
instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
)
const settingsModal = ref()
const timePlayed = computed(() => {
return instance.value.recent_time_played + instance.value.submitted_time_played
})
const timePlayedHumanized = computed(() => {
const duration = dayjs.duration(timePlayed.value, 'seconds')
const hours = Math.floor(duration.asHours())
if (hours >= 1) {
return hours + ' hour' + (hours > 1 ? 's' : '')
}
const minutes = Math.floor(duration.asMinutes())
if (minutes >= 1) {
return minutes + ' minute' + (minutes > 1 ? 's' : '')
}
const seconds = Math.floor(duration.asSeconds())
return seconds + ' second' + (seconds > 1 ? 's' : '')
})
onUnmounted(() => {
unlistenProcesses()
unlistenProfiles()
})
</script>
<style scoped lang="scss">
.instance-card {
display: flex;
flex-direction: column;
gap: 1rem;
}
Button {
width: 100%;
}
.button-group {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.side-cards {
position: fixed;
width: 300px;
display: flex;
flex-direction: column;
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
.card {
min-height: unset;
margin-bottom: 0;
}
}
.instance-nav {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 1rem;
gap: 0.5rem;
background: var(--color-raised-bg);
height: 100%;
}
.name {
font-size: 1.25rem;
color: var(--color-contrast);
overflow: hidden;
text-overflow: ellipsis;
}
.metadata {
text-transform: capitalize;
}
.instance-container {
display: flex;
flex-direction: row;
overflow: auto;
gap: 1rem;
min-height: 100%;
padding: 1rem;
}
.instance-info {
display: flex;
flex-direction: column;
width: 100%;
}
.badge {
display: flex;
align-items: center;
font-weight: bold;
width: fit-content;
color: var(--color-orange);
}
.pages-list {
display: flex;
flex-direction: column;
gap: var(--gap-xs);
.btn {
font-size: 100%;
font-weight: 400;
background: inherit;
transition: all ease-in-out 0.1s;
width: 100%;
color: var(--color-primary);
box-shadow: none;
&.router-link-exact-active {
box-shadow: var(--shadow-inset-lg);
background: var(--color-button-bg);
color: var(--color-contrast);
}
&:hover {
background-color: var(--color-button-bg);
color: var(--color-contrast);
box-shadow: var(--shadow-inset-lg);
text-decoration: none;
}
svg {
width: 1.3rem;
height: 1.3rem;
}
}
}
.instance-nav {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: left;
padding: 1rem;
gap: 0.5rem;
height: min-content;
width: 100%;
}
.instance-button {
width: fit-content;
}
.actions {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 0.5rem;
}
.content {
margin: 0 1rem 0.5rem 20rem;
width: calc(100% - 20rem);
display: flex;
flex-direction: column;
overflow: auto;
}
.stats {
grid-area: stats;
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: var(--gap-md);
.stat {
display: flex;
flex-direction: row;
align-items: center;
width: fit-content;
gap: var(--gap-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(--gap-md);
margin-top: var(--gap-xs);
}
@media screen and (max-width: 600px) {
margin-top: 0;
.stat-label {
display: none;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
<script setup>
import GridDisplay from '@/components/GridDisplay.vue'
defineProps({
instances: {
type: Array,
required: true,
},
})
</script>
<template>
<GridDisplay
v-if="instances.length > 0"
label="Instances"
:instances="instances.filter((i) => !i.linked_data)"
/>
</template>

View File

@@ -1,17 +0,0 @@
<script setup>
import GridDisplay from '@/components/GridDisplay.vue'
defineProps({
instances: {
type: Array,
required: true,
},
})
</script>
<template>
<GridDisplay
v-if="instances.length > 0"
label="Instances"
:instances="instances.filter((i) => i.linked_data)"
/>
</template>

View File

@@ -1,87 +0,0 @@
<script setup>
import { onUnmounted, ref, shallowRef } from 'vue'
import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
import { profile_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js'
import { Button } from '@modrinth/ui'
import { PlusIcon } from '@modrinth/assets'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { NewInstanceImage } from '@/assets/icons'
import NavTabs from '@/components/ui/NavTabs.vue'
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
const instances = shallowRef(await list().catch(handleError))
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const unlistenProfile = await profile_listener(async () => {
instances.value = await list().catch(handleError)
})
onUnmounted(() => {
unlistenProfile()
})
</script>
<template>
<div class="p-6 flex flex-col gap-3">
<h1 class="m-0 text-2xl hidden">Library</h1>
<NavTabs
:links="[
{ label: 'All instances', href: `/library` },
{ label: 'Downloaded', href: `/library/downloaded` },
{ label: 'Custom', href: `/library/custom` },
{ label: 'Shared with me', href: `/library/shared`, shown: false },
{ label: 'Saved', href: `/library/saved`, shown: false },
]"
/>
<template v-if="instances.length > 0">
<RouterView :instances="instances" />
</template>
<div v-else class="no-instance">
<div class="icon">
<NewInstanceImage />
</div>
<h3>No instances found</h3>
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
<PlusIcon />
Create new instance
</Button>
<InstanceCreationModal ref="installationModal" />
</div>
</div>
</template>
<style lang="scss" scoped>
.no-instance {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--gap-md);
p,
h3 {
margin: 0;
}
.icon {
svg {
width: 10rem;
height: 10rem;
}
}
}
</style>

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