69 Commits

Author SHA1 Message Date
d83f839ba4 Merge pull request 'Remove unnecessary workflows' (#23) from beta into release
Reviewed-on: #23
2025-11-02 10:08:25 +03:00
9139c23469 Remove unnecessary workflows 2025-11-02 10:08:03 +03:00
c1d7d66ab0 Merge pull request 'Update README markdown language files' (#22) from beta into release
Reviewed-on: #22
2025-11-02 10:06:13 +03:00
932f4ce662 Update README markdown language files
Some checks failed
/ typos (pull_request) Failing after 24s
/ tombi (pull_request) Successful in 21s
/ shear (pull_request) Has been cancelled
2025-11-02 10:05:22 +03:00
1fbd39c920 Update README markdown language files 2025-11-02 10:03:17 +03:00
78d2652b08 Merge pull request 'Merge beta into release' (#21) from beta into release
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 36m46s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
Reviewed-on: #21
2025-11-01 16:04:23 +03:00
27abe2b42f Upgrade JDK version
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 40m21s
/ typos (pull_request) Failing after 1m5s
/ tombi (pull_request) Successful in 21s
/ shear (pull_request) Failing after 2m28s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-11-01 15:01:58 +03:00
ece15a97a0 Update tauri configurations and CI build file
Some checks failed
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Has been cancelled
2025-11-01 14:31:26 +03:00
97a9c24768 Merge tag 'v0.10.16' into beta 2025-11-01 14:14:52 +03:00
Prospector
3ac3122b31 Hide locked filter controls if they've been overridden (#4682) 2025-10-30 15:57:21 -07:00
Rayzeq
6b5f8a41e7 feat: split wrapper command on linux (#4427)
* feat: split wrapper command on linux

* feat: use code from #3900

* feat: also use shlex on Windows

* feat: add a version number to global settings

* feat(app): add settings v2, where wrapper command are split
2025-10-30 21:48:51 +00:00
Calum H.
8b39ba491a fix: keybind issues with gallery + moderation checklist (#4674) 2025-10-30 21:20:16 +00:00
Calum H.
c74460fffa fix: firefox backup download issues (#4679)
* fix: firefox backup download issues

* fix: lint

* fix: hide download button when downloading & disable overflow menu options
2025-10-30 21:20:08 +00:00
Alejandro González
5000c4067b fix(app-lib): stricter override file path validation (#4681) 2025-10-30 21:19:23 +00:00
Calum H.
af33950bbe fix: race condition for chart query param (#4677) 2025-10-30 19:19:44 +00:00
Calum H.
075331b26c fix: remove reply-to references in email templates (#4676) 2025-10-30 19:19:20 +00:00
Calum H.
f31b74f7fd fix: show hostname on modrinth servers 503 (#4678)
* fix: show hostname on modrinth servers 503

* fix: lint
2025-10-30 19:18:24 +00:00
aecsocket
bcc36362be Expose utilities for setting up the database (#4657)
* Expose utilities for setting up the database

* Expose migrator directly

* Make some test utils publicly accessible

* expose migrator

* more test fixture utils

* more test fixture utils

* more test fixture utils

* fix

* fix lint
2025-10-30 10:10:25 +00:00
Jerozgen
632b27dc21 Fix "Add friends" link (#4663) 2025-10-29 22:02:28 +00:00
Prospector
cf6f3736eb Update ads.txt (#4670) 2025-10-29 21:59:16 +00:00
Prospector
aaaef8f39e Fix double friends label, Closes #4655 (#4656)
* Fix double friends label, Closes #4655

* lint
2025-10-29 21:59:07 +00:00
Prospector
3f8dd1a79c changelog 2025-10-27 16:21:36 -07:00
Calum H.
363f47f269 feat: blog (#4653)
* feat: blog

* feat: creator withdraw overhaul blog

* fix: bullet points

* fix: title

* fix: blog

* feat: autoplay on scroll & fix encoding of videos

* fix: class on first video

* fix: authors + summary + title

* fix: title + summary

* fix: lint

* fix: rev page mp4

* update formatting + phrasing

* some more formatting changes

* unify hr colors

* update opening line

* update blog time

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-10-27 16:19:35 -07:00
Jerozgen
a0f23a2bca Fix normalized skins uploading to Mojang (#4646)
* Fix normalized skins uploading to Mojang

* Run app-frontend > fix
2025-10-26 13:53:56 +00:00
Jerozgen
08e316a2b2 Move skin preview directional light (#4649) 2025-10-26 12:56:46 +00:00
Prospector
9aaf5fb87e update changelog 2025-10-24 20:55:58 -07:00
Prospector
bcca66b12c Fix OLED colors (#4638)
* Make OLED theme proper dark again, shifting surface vars up one.

* Revert tertiary text color
2025-10-24 22:45:04 +00:00
Alejandro González
ccb24ce8eb chore(vscode): use Rust analyzer rustfmt for formatting (#4637) 2025-10-24 21:37:02 +00:00
Prospector
5dd6c804d0 fix padding issues (#4604) 2025-10-24 18:58:20 +00:00
aecsocket
ab886a5ea8 Fix CORS (#4610) 2025-10-24 18:27:44 +00:00
aecsocket
03b0eba695 Add utoipa Swagger UI support (#4602)
* Add utoipa Swagger UI support

* remove unused code

* remove unused code

* consistency with trailing slash
2025-10-24 14:44:50 +00:00
aecsocket
707ff2146b Update appropriate rows when removing a user (#4597)
* Update appropriate rows when removing a user

* Update sqlx cache

* Delete rows from payouts_values_notifications instead of make ghost user
2025-10-24 14:19:53 +00:00
Prospector
8d80433c2c Update 3-servers-bug.yml (#4607)
Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-10-22 18:15:41 -07:00
Prospector
a547f7a9b0 Update issue templates (#4606)
* Update 1-app-bug.yml

Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>

* update the rest of the templates

* Update issue template formatting further

* Disable blank issue + get rid of some contact links

* fix issue location id

* more updates

---------

Signed-off-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-10-22 18:13:49 -07:00
Calum H.
f78fbe3215 fix: disable start button on backup restore/create (#4582)
* fix: CLAUDE.md

* fix: allowing start server on backup create/restore

---------

Signed-off-by: Calum H. <contact@cal.engineer>
2025-10-22 16:25:55 +00:00
François-Xavier Talbot
f375913c62 Adjust some values in tax-related tasks (#4598)
* Adjust some values for tax processing

* chore: query cache, clippy, fmt
2025-10-21 15:55:54 +00:00
François-Xavier Talbot
a4015d9df3 Fix v1 servers handling (#4596) 2025-10-21 06:40:10 +00:00
François-Xavier Talbot
977de0e18a Fix MaxMind (#4595)
* add maxmind to app data

* add back maxmind account id check
2025-10-21 00:24:47 +02:00
François-Xavier Talbot
c379e4b173 admin/credit: don't credit unprovisioned subscriptions (#4594)
* Remove pointless sorting

* Filter subscriptions by labrinth's provisioned state
2025-10-20 20:31:20 +00:00
François-Xavier Talbot
eeed4e572d Credit subscriptions (#4575)
* Implement subscription crediting

* chore: query cache, clippy, fmt

* Improve code, improve query for next open charge

* chore: query cache, clippy, fmt

* Move server ID copy button up

* Node + region crediting

* Make it less ugly

* chore: query cache, clippy, fmt

* Bugfixes

* Fix lint

* Adjust migration

* Adjust migration

* Remove billing change

* Move DEFAULT_CREDIT_EMAIL_MESSAGE to utils.ts

* Lint

* Merge

* bump clickhouse, disable validation

* tombi fmt

* Update cargo lock
2025-10-20 17:35:44 +00:00
François-Xavier Talbot
79502a19d6 bump clickhouse, disable validation (#4593)
* bump clickhouse, disable validation

* tombi fmt
2025-10-20 17:30:28 +00:00
François-Xavier Talbot
3dbfd69bdd Fix clickhouse (#4592) 2025-10-20 19:07:23 +02:00
Michael H.
19393a38bb fix(build): build on cargo.toml modifications 2025-10-20 18:59:02 +02:00
François-Xavier Talbot
859d7f57cf Downgrade CH dep (#4591) 2025-10-20 18:50:16 +02:00
aecsocket
24bec6baba Fix MaxMind (#4590) 2025-10-20 15:58:31 +00:00
Jerozgen
63d8f70e20 Fix friends texts (#4587) 2025-10-20 14:56:44 +00:00
François-Xavier Talbot
8a30b7978d Support ctoken_ in PATCH subscription (#4578) 2025-10-20 10:03:20 +00:00
Drew Chase
4a9f0b8a0e Include MAXIMIZED state flag in window state handling (#4566) 2025-10-20 01:00:11 +00:00
Calum H.
0e17427a58 fix: #4568 & i18n on user page (#4572)
* fix: #4568

* fix: lint
2025-10-20 00:23:06 +00:00
Prospector
ad3b5aec69 update changelog 2025-10-19 17:32:22 -07:00
François-Xavier Talbot
4b17eb5d35 Gotenberg/PDF gen implementation (#4574)
* Gotenberg/PDF gen implementation

* Security, PDF type enum, propagate client

* chore: query cache, clippy, fmt

* clippy fixes + tombi

* Update env example, add GOTENBERG_CALLBACK_URL

* Remove test code

* Fix .env, docker-compose

* Update purpose of payment

* Add internal networking guards to gotenberg webhooks

* Fix error

* Fix lint
2025-10-19 23:56:26 +00:00
Prospector
6a70acef25 Updated ad placeholder graphics, update Modrinth App sidebar to mockup designs (#4584)
* Update ad placeholders to new green graphic

* Remove rounded corners from app ad frame

* Improve web ad placeholder styling

* Revamp app sidebar to match mockups more closely, greatly improve friends UX, fix up context menus and typify shit

* only show overflow on hover

* lint

* intl:extract

* clean up the inline code in FriendsSection
2025-10-19 23:26:17 +00:00
Prospector
e58456eed4 Fix using the wrong icon for open folder on instance ctx menu (#4586) 2025-10-19 23:18:27 +00:00
Calum H.
12940fc207 fix: default subscription interval in servers upgrade modal wrapper (#4585) 2025-10-19 23:16:13 +00:00
François-Xavier Talbot
7796273529 Clearer error on TIN mismatch (#4579)
* Clearer error on TIN mismatch

* Remove ctoken code (how did that end up there)

* polish: frontend for TIN/SSN mismatch

* fix: lint

* polish: only banner + change text

* fix: logic

* fix: lint

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
2025-10-19 21:03:21 +00:00
7cc9d8183d fix: update.js top level awaiting
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 36m27s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
Crowdin (pull) / Pull translations from Crowdin (push) Has been skipped
2025-10-19 21:48:45 +03:00
231e95792e update README
Some checks failed
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Has been cancelled
2025-10-19 21:31:10 +03:00
905eae8403 cleanup patches 2025-10-19 21:08:52 +03:00
868fda1703 fix typo: remove shit ads after upstream 2025-10-19 21:08:02 +03:00
4b86c4ee8a refactor: minor changes in update.js 2025-10-19 21:06:03 +03:00
Michael H.
752f68124c fix: sync debian version with rust image 2025-10-19 18:11:41 +02:00
Calum H.
699a049c69 fix: medal upgrade for new payment methods (#4581) 2025-10-19 12:31:15 +00:00
aecsocket
fa7d1d7942 Use new MaxMind env vars on Labrinth (#4573)
* Bring in modrinth-maxmind

* integrate modrinth-maxmind into labrinth

* Fix CI
2025-10-18 18:38:19 +00:00
Calum H.
d1ffed564d fix: #4567 (#4571) 2025-10-17 17:56:25 +00:00
Alejandro González
e719ae2f42 fix(daedalus-client): backport new Mojang MC version library patches from PrismLauncher (#4493)
While researching and fixing other issue, it caught my attention that we
are embedding a library patches JSON file from the PrismLauncher meta
repository. However, since we copied that file, a new revision of it was
published with patches that improve compatibility with Apple Silicon
macOS platforms.

These changes update such a file and, perhaps most importantly, add a
comment explaining the provenance and licensing of such a file.
2025-10-17 16:43:04 +00:00
François-Xavier Talbot
5db5bf4c4c Changes to handling of refunds in Anrok (#4556)
* Use negations, track transaction version/accounting time, use original charge accounting time in refunds

* query cache

* chore: query cache, clippy, fmt

* Fix tax drift calculation

* Fix migration

* Increase update_tax_transactions rate
2025-10-17 15:57:36 +00:00
Josiah Glosson
b23d3e674f Update Rust & Java dependencies (#4540)
* Update Java dependencies

* Baselint lint fixes

* Update Rust version

* Update actix-files 0.6.6 -> 0.6.8

* Update actix-http 3.11.0 -> 3.11.2

* Update actix-rt 2.10.0 -> 2.11.0

* Update async_zip 0.0.17 -> 0.0.18

* Update async-compression 0.4.27 -> 0.4.32

* Update async-trait 0.1.88 -> 0.1.89

* Update async-tungstenite 0.30.0 -> 0.31.0

* Update const_format 0.2.34 -> 0.2.35

* Update bitflags 2.9.1 -> 2.9.4

* Update bytemuck 1.23.1 -> 1.24.0

* Update typed-path 0.11.0 -> 0.12.0

* Update chrono 0.4.41 -> 0.4.42

* Update cidre 0.11.2 -> 0.11.3

* Update clap 4.5.43 -> 4.5.48

* Update data-url 0.3.1 -> 0.3.2

* Update discord-rich-presence 0.2.5 -> 1.0.0

* Update enumset 1.1.7 -> 1.1.10

* Update flate2 1.1.2 -> 1.1.4

* Update hyper 1.6.0 -> 1.7.0

* Update hyper-util 0.1.16 -> 0.1.17

* Update iana-time-zone 0.1.63 -> 0.1.64

* Update image 0.25.6 -> 0.25.8

* Update indexmap 2.10.0 -> 2.11.4

* Update json-patch 4.0.0 -> 4.1.0

* Update meilisearch-sdk 0.29.1 -> 0.30.0

* Update clickhouse 0.13.3 -> 0.14.0

* Fix some prettier things

* Update lettre 0.11.18 -> 0.11.19

* Update phf 0.12.1 -> 0.13.1

* Update png 0.17.16 -> 0.18.0

* Update quick-xml 0.38.1 -> 0.38.3

* Update redis 0.32.4 -> 0.32.7

* Update regex 1.11.1 -> 1.11.3

* Update reqwest 0.12.22 -> 0.12.23

* Update rust_decimal 1.37.2 -> 1.38.0

* Update rust-s3 0.35.1 -> 0.37.0

* Update serde 1.0.219 -> 1.0.228

* Update serde_bytes 0.11.17 -> 0.11.19

* Update serde_json 1.0.142 -> 1.0.145

* Update serde_with 3.14.0 -> 3.15.0

* Update sentry 0.42.0 -> 0.45.0 and sentry-actix 0.42.0 -> 0.45.0

* Update spdx 0.10.9 -> 0.12.0

* Update sysinfo 0.36.1 -> 0.37.2

* Update tauri 2.7.0 -> 2.8.5

* Update tauri-build 2.3.1 -> 2.4.1

* Update tauri-plugin-deep-link 2.4.1 -> 2.4.3

* Update tauri-plugin-dialog 2.3.2 -> 2.4.0

* Update tauri-plugin-http 2.5.1 -> 2.5.2

* Update tauri-plugin-opener 2.4.0 -> 2.5.0

* Update tauri-plugin-os 2.3.0 -> 2.3.1

* Update tauri-plugin-single-instance 2.3.2 -> 2.3.4

* Update tempfile 3.20.0 -> 3.23.0

* Update thiserror 2.0.12 -> 2.0.17

* Update tracing-subscriber 0.3.19 -> 0.3.20

* Update url 2.5.4 -> 2.5.7

* Update uuid 1.17.0 -> 1.18.1

* Update webp 0.3.0 -> 0.3.1

* Update whoami 1.6.0 -> 1.6.1

* Note that windows and windows-core can't be updated yet

* Update zbus 5.9.0 -> 5.11.0

* Update zip 4.3.0 -> 6.0.0

* Fix build

* Enforce rustls crypto provider

* Refresh Cargo.lock

* Update transitive dependencies

* Bump Gradle usage to Java 17

* Use ubuntu-latest consistently across workflows

* Fix lint

* Fix lint in Rust

* Update native-dialog 0.9.0 -> 0.9.2

* Update regex 1.11.3 -> 1.12.2

* Update reqwest 0.12.23 -> 0.12.24

* Update rust_decimal 1.38.0 -> 1.39.0

* Remaining lock-only updates

* chore: move TLS impl of some other dependencies to aws-lc-rs

The AWS bloatware "virus" expands by sheer force of widespread adoption
by the ecosystem... 🫣

* chore(fmt): run Tombi

---------

Co-authored-by: Alejandro González <me@alegon.dev>
2025-10-15 20:45:47 +00:00
e3065c2dfb Merge pull request 'beta' (#14) from beta into release
Reviewed-on: #14
2025-08-17 00:13:38 +03:00
f54f09becf Merge pull request 'beta' (#12) from beta into release
Reviewed-on: #12
2025-07-26 12:49:02 +03:00
220 changed files with 6487 additions and 9953 deletions

View File

@@ -0,0 +1,63 @@
name: 👥 Bug with Modrinth Servers
description: For issues with a Modrinth Servers product.
labels: [servers]
type: 'bug'
body:
- type: checkboxes
attributes:
label: Please confirm the following.
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) 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: issue-location
attributes:
label: Is this an issue in the control panel or with the Minecraft server itself?
options:
- Control panel (on Modrinth.com)
- Minecraft server
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on? (if a panel issue)
multiple: true
options:
- N/A
- 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,14 +1,8 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: 🫶 Support Portal
about: Get support using through our portal.
- name: 🫶 Support portal
about: Get support using through our support website.
url: https://support.modrinth.com
- name: 💬 Chat
- name: 💬 Chat on Discord
about: Join our Discord server to chat about Modrinth.
url: https://discord.modrinth.com
- 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
url: https://docs.modrinth.com

View File

@@ -100,7 +100,7 @@ jobs:
libayatana-appindicator3-dev \
librsvg2-dev \
xdg-utils \
openjdk-11-jdk
openjdk-17-jdk
- name: ⚙️ Set application environment
shell: bash
@@ -143,7 +143,7 @@ jobs:
if: matrix.platform == 'windows-latest'
shell: pwsh
run: |
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}

View File

@@ -1,24 +0,0 @@
on:
pull_request:
push:
branches:
- master
env:
CARGO_TERM_COLOR: always
SQLX_OFFLINE: true
jobs:
typos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
tombi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: tombi-toml/setup-tombi@v1
- run: tombi lint
- run: tombi fmt --check

View File

@@ -1,19 +0,0 @@
on:
pull_request:
push:
branches:
- master
env:
CARGO_TERM_COLOR: always
SQLX_OFFLINE: true
jobs:
shear:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: cargo-bins/cargo-binstall@main
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear

View File

@@ -1,108 +0,0 @@
name: Crowdin (pull)
on:
schedule:
- cron: '0 7 * * MON' # every monday at 7 am
workflow_dispatch:
concurrency:
group: i18n-management
jobs:
pull_translations:
name: 'Pull translations from Crowdin'
runs-on: ubuntu-22.04
if: github.ref == 'refs/heads/main'
concurrency:
group: i18n-pull:${{ github.ref }}
cancel-in-progress: true
steps:
- name: Preflight check
run: |
PREFLIGHT_CHECK_RESULT=true
function flight_failure () {
if [ "$PREFLIGHT_CHECK_RESULT" = true ]; then
echo "One or more pre-flight checks failed!"
echo ""
PREFLIGHT_CHECK_RESULT=false
fi
echo "- $1"
}
if [ "$CROWDIN_PROJECT_ID_DEFINED" != true ]; then
flight_failure "CROWDIN_PROJECT_ID variable is not defined (required to push)"
fi
if [ "$CROWDIN_PERSONAL_TOKEN_DEFINED" != true ]; then
flight_failure "CROWDIN_PERSONAL_TOKEN secret is not defined (required to push)"
fi
if [ "$CROWDIN_GH_TOKEN_DEFINED" != true ]; then
flight_failure "CROWDIN_GH_TOKEN secret is not defined (required to make pull requests)"
fi
if [ "$PREFLIGHT_CHECK_RESULT" = false ]; then
exit 1
fi
env:
CROWDIN_PROJECT_ID_DEFINED: ${{ vars.CROWDIN_PROJECT_ID != '' }}
CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }}
CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }}
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
token: ${{ secrets.CROWDIN_GH_TOKEN }}
- name: Configure Git author
id: git-author
uses: MarcoIeni/git-config@v0.1
env:
GITHUB_TOKEN: ${{ secrets.CROWDIN_GH_TOKEN }}
# # Because --all flag of Crowdin CLI is currently broken we need to create a fake source file
# # so that the CLI won't omit translations for it. See https://github.com/crowdin/crowdin-cli/issues/724
# - name: Write fake sources
# shell: bash
# run: echo "{}" > locales/en-US/index.json
- name: Query branch name
id: branch-name
shell: bash
run: |
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed -e "s/[\\\\/\\:*?\"<>|]/_/g")
echo "Branch name is $BRANCH_NAME (escaped as $SAFE_BRANCH_NAME)"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Download translations from Crowdin
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
push_translations: false
create_pull_request: false
crowdin_branch_name: '[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}'
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Fix broken permissions
shell: bash
run: sudo chown -R $USER:$USER .
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
title: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})'
body-path: .github/templates/crowdin-pr.md
commit-message: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})'
branch: crowdin-pull/${{ steps.branch-name.outputs.branch_name }}
author: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>'
committer: '${{ steps.git-author.outputs.name }} <${{ steps.git-author.outputs.email }}>'
labels: sync
token: ${{ secrets.CROWDIN_GH_TOKEN }}

View File

@@ -1,81 +0,0 @@
name: Crowdin (push)
on:
push:
branches: ['main']
paths:
- '.github/workflows/i18n.push.yml'
- 'apps/*/src/locales/en-US/**'
- 'apps/*/locales/en-US/**'
- 'packages/*/src/locales/en-US/**'
- 'packages/*/locales/en-US/**'
- 'crowdin.yml'
workflow_dispatch:
concurrency:
group: i18n-management
jobs:
push_translations:
name: Push sources to Crowdin
runs-on: ubuntu-22.04
if: github.ref == 'refs/heads/main'
concurrency:
group: i18n-push:${{ github.ref }}
cancel-in-progress: true
steps:
- name: Preflight check
run: |
PREFLIGHT_CHECK_RESULT=true
function flight_failure () {
if [ "$PREFLIGHT_CHECK_RESULT" = true ]; then
echo "One or more pre-flight checks failed!"
echo ""
PREFLIGHT_CHECK_RESULT=false
fi
echo "- $1"
}
if [ "$CROWDIN_PROJECT_ID_DEFINED" != true ]; then
flight_failure "CROWDIN_PROJECT_ID variable is not defined (required to push)"
fi
if [ "$CROWDIN_PERSONAL_TOKEN_DEFINED" != true ]; then
flight_failure "CROWDIN_PERSONAL_TOKEN secret is not defined (required to push)"
fi
if [ "$PREFLIGHT_CHECK_RESULT" = false ]; then
exit 1
fi
env:
CROWDIN_PROJECT_ID_DEFINED: ${{ vars.CROWDIN_PROJECT_ID != '' }}
CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }}
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Query branch name
id: branch-name
shell: bash
run: |
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed -e "s/[\\\\/\\:*?\"<>|]/_/g")
echo "Branch name is $BRANCH_NAME (escaped as $SAFE_BRANCH_NAME)"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Upload translations to Crowdin
uses: crowdin/github-action@v1
with:
upload_sources: true
upload_translations: false
download_translations: false
push_translations: false
create_pull_request: false
crowdin_branch_name: '[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}'
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

20
.vscode/settings.json vendored
View File

@@ -11,5 +11,23 @@
"source.fixAll.eslint": "explicit",
"source.organizeImports": "always"
},
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}

View File

@@ -1,5 +1,35 @@
# Architecture
Use TAB instead of spaces.
## Frontend
There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend).
Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively.
Both utilize shared and common components from `@modrinth/ui` which can be found at `packages/ui`, and stylings from `@modrinth/assets` which can be found at `packages/assets`.
Both can utilize icons from `@modrinth/assets`, which are automatically generated based on what's available within the `icons` folder of the `packages/assets` directory. You can see the generated icons list in `generated-icons.ts`.
Both have access to our dependency injection framework, examples as seen in `packages/ui/src/providers/`. Ideally any state which is shared between a page and it's subpages should be shared using this dependency injection framework.
### Website (apps/frontend)
Before a pull request can be opened for the website, `pnpm web:fix` and `pnpm web:intl:extract` must be run, otherwise CI will fail.
To run a development version of the frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within the `apps/frontend` folder into `apps/frontend/.env`. Then you can run the frontend by running `pnpm web:dev` in the root folder.
### App Frontend (apps/app-frontend)
Before a pull request can be opened for the website, you must CD into the `app-frontend` folder; `pnpm fix` and `pnpm intl:extract` must be run, otherwise CI will fail.
To run a development version of the app frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within `packages/app-lib` into `packages/app-lib/.env`. Then you must run the app itself by running `pnpm app:dev` in the root folder.
### Localization
Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks you to perform any i18n conversion work on a component, set of components, pages or sets of pages.
## Labrinth
Labrinth is the backend API service for Modrinth.
@@ -15,6 +45,7 @@ To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`.
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
When the user refers to "performing pre-PR checks", do the following:
- Run clippy as described above
- DO NOT run tests unless explicitly requested (they take a long time)
- Prepare the sqlx cache

1209
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,92 +8,97 @@ members = [
"packages/app-lib",
"packages/ariadne",
"packages/daedalus",
"packages/modrinth-maxmind",
"packages/modrinth-util",
"packages/path-util",
]
[workspace.package]
edition = "2024"
rust-version = "1.90.0"
repository = "https://github.com/modrinth/code"
[workspace.dependencies]
actix-cors = "0.7.1"
actix-files = "0.6.6"
actix-http = "3.11.0"
actix-files = "0.6.8"
actix-http = "3.11.2"
actix-multipart = "0.7.2"
actix-rt = "2.10.0"
actix-rt = "2.11.0"
actix-web = "4.11.0"
actix-web-prom = "0.10.0"
actix-ws = "0.3.0"
argon2 = { version = "0.5.3", features = ["std"] }
ariadne = { path = "packages/ariadne" }
async-compression = { version = "0.4.27", default-features = false }
async-compression = { version = "0.4.32", default-features = false }
async-recursion = "1.1.1"
async-stripe = { version = "0.41.0", default-features = false, features = [
"runtime-tokio-hyper-rustls",
] }
async-trait = "0.1.88"
async-tungstenite = { version = "0.30.0", default-features = false, features = [
"futures-03-sink",
async-trait = "0.1.89"
async-tungstenite = { version = "0.31.0", default-features = false, features = [
"futures-03-sink"
] }
async-walkdir = "2.1.0"
async_zip = "0.0.17"
async_zip = "0.0.18"
base64 = "0.22.1"
bitflags = "2.9.1"
bytemuck = "1.23.1"
bitflags = "2.9.4"
bytemuck = "1.24.0"
bytes = "1.10.1"
censor = "0.3.0"
chardetng = "0.1.17"
chrono = "0.4.41"
cidre = { version = "0.11.2", default-features = false, features = [
"macos_15_0",
chrono = "0.4.42"
cidre = { version = "0.11.3", default-features = false, features = [
"macos_15_0"
] }
clap = "4.5.43"
clickhouse = "0.13.3"
clap = "4.5.48"
clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
console-subscriber = "0.4.1"
const_format = "0.2.34"
daedalus = { path = "packages/daedalus" }
dashmap = "6.1.0"
data-url = "0.3.1"
data-url = "0.3.2"
deadpool-redis = "0.22.0"
derive_more = "2.0.1"
directories = "6.0.0"
dirs = "6.0.0"
discord-rich-presence = "0.2.5"
discord-rich-presence = "1.0.0"
dotenv-build = "0.1.1"
dotenvy = "0.15.7"
dunce = "1.0.5"
either = "1.15.0"
encoding_rs = "0.8.35"
enumset = "1.1.7"
enumset = "1.1.10"
eyre = "0.6.12"
flate2 = "1.1.2"
flate2 = "1.1.4"
fs4 = { version = "0.13.1", default-features = false }
futures = { version = "0.3.31", default-features = false }
futures = "0.3.31"
futures-util = "0.3.31"
heck = "0.5.0"
hex = "0.4.3"
hickory-resolver = "0.25.2"
hmac = "0.12.1"
hyper = "1.6.0"
hyper = "1.7.0"
hyper-rustls = { version = "0.27.7", default-features = false, features = [
"aws-lc-rs",
"http1",
"native-tokio",
"ring",
"tls12",
] }
hyper-util = "0.1.16"
iana-time-zone = "0.1.63"
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
indexmap = "2.10.0"
hyper-util = "0.1.17"
iana-time-zone = "0.1.64"
image = { version = "0.25.8", default-features = false, features = ["rayon"] }
indexmap = "2.11.4"
indicatif = "0.18.0"
itertools = "0.14.0"
jemalloc_pprof = "0.8.1"
json-patch = { version = "4.0.0", default-features = false }
lettre = { version = "0.11.18", default-features = false, features = [
json-patch = { version = "4.1.0", default-features = false }
lettre = { version = "0.11.19", default-features = false, features = [
"aws-lc-rs",
"builder",
"hostname",
"pool",
"ring",
"rustls",
"rustls-native-certs",
"smtp-transport",
@@ -101,37 +106,40 @@ lettre = { version = "0.11.18", default-features = false, features = [
"tokio1-rustls",
] }
maxminddb = "0.26.0"
meilisearch-sdk = { version = "0.29.1", default-features = false }
meilisearch-sdk = { version = "0.30.0", default-features = false }
modrinth-maxmind = { path = "packages/modrinth-maxmind" }
modrinth-util = { path = "packages/modrinth-util" }
murmur2 = "0.1.0"
native-dialog = "0.9.0"
native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
notify-debouncer-mini = { version = "0.7.0", default-features = false }
p256 = "0.13.2"
paste = "1.0.15"
path-util = { path = "packages/path-util" }
phf = { version = "0.12.1", features = ["macros"] }
png = "0.17.16"
phf = { version = "0.13.1", features = ["macros"] }
png = "0.18.0"
prometheus = "0.14.0"
quartz_nbt = "0.2.9"
quick-xml = "0.38.1"
quick-xml = "0.38.3"
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
redis = "0.32.4"
regex = "1.11.1"
reqwest = { version = "0.12.22", default-features = false }
redis = "0.32.7"
regex = "1.12.2"
reqwest = { version = "0.12.24", default-features = false }
rgb = "0.8.52"
rust_decimal = { version = "1.37.2", features = [
rust_decimal = { version = "1.39.0", features = [
"serde-with-float",
"serde-with-str",
"serde-with-str"
] }
rust_iso3166 = "0.1.14"
rust-s3 = { version = "0.35.1", default-features = false, features = [
rust-s3 = { version = "0.37.0", default-features = false, features = [
"fail-on-err",
"tags",
"tokio-rustls-tls",
] }
rustls = "0.23.32"
rusty-money = "0.4.1"
sentry = { version = "0.42.0", default-features = false, features = [
sentry = { version = "0.45.0", default-features = false, features = [
"backtrace",
"contexts",
"debug-images",
@@ -139,37 +147,38 @@ sentry = { version = "0.42.0", default-features = false, features = [
"reqwest",
"rustls",
] }
sentry-actix = "0.42.0"
serde = "1.0.219"
serde_bytes = "0.11.17"
sentry-actix = "0.45.0"
serde = "1.0.228"
serde_bytes = "0.11.19"
serde_cbor = "0.11.2"
serde_ini = "0.2.0"
serde_json = "1.0.142"
serde_with = "3.14.0"
serde_json = "1.0.145"
serde_with = "3.15.0"
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
sha1 = "0.10.6"
sha1_smol = { version = "1.0.1", features = ["std"] }
sha2 = "0.10.9"
spdx = "0.10.9"
shlex = "1.3.0"
spdx = "0.12.0"
sqlx = { version = "0.8.6", default-features = false }
sysinfo = { version = "0.36.1", default-features = false }
sysinfo = { version = "0.37.2", default-features = false }
tar = "0.4.44"
tauri = "2.7.0"
tauri-build = "2.3.1"
tauri-plugin-deep-link = "2.4.1"
tauri-plugin-dialog = "2.3.2"
tauri-plugin-http = "2.5.1"
tauri-plugin-opener = "2.4.0"
tauri-plugin-os = "2.3.0"
tauri-plugin-single-instance = "2.3.2"
tauri = "2.8.5"
tauri-build = "2.4.1"
tauri-plugin-deep-link = "2.4.3"
tauri-plugin-dialog = "2.4.0"
tauri-plugin-http = "2.5.2"
tauri-plugin-opener = "2.5.0"
tauri-plugin-os = "2.3.1"
tauri-plugin-single-instance = "2.3.4"
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
"rustls-tls",
"zip",
] }
tauri-plugin-window-state = "2.4.0"
tempfile = "3.20.0"
tempfile = "3.23.0"
theseus = { path = "packages/app-lib" }
thiserror = "2.0.12"
thiserror = "2.0.17"
tikv-jemalloc-ctl = "0.6.0"
tikv-jemallocator = "0.6.0"
tokio = "1.47.1"
@@ -180,22 +189,25 @@ tracing = "0.1.41"
tracing-actix-web = { version = "0.7.19", default-features = false }
tracing-ecs = "0.5.0"
tracing-error = "0.2.1"
tracing-subscriber = "0.3.19"
typed-path = "0.11.0"
url = "2.5.4"
tracing-subscriber = "0.3.20"
typed-path = "0.12.0"
url = "2.5.7"
urlencoding = "2.1.3"
uuid = "1.17.0"
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
utoipa-actix-web = { version = "0.1.2" }
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web", "vendored"] }
uuid = "1.18.1"
validator = "0.20.0"
webp = { version = "0.3.0", default-features = false }
webp = { version = "0.3.1", default-features = false }
webview2-com = "0.38.0" # Should be updated in lockstep with wry
whoami = "1.6.0"
windows = "0.61.3"
windows-core = "0.61.2"
whoami = "1.6.1"
windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
winreg = "0.55.0"
woothee = "0.13.0"
yaserde = "0.12.0"
zbus = "5.9.0"
zip = { version = "4.3.0", default-features = false, features = [
zbus = "5.11.0"
zip = { version = "6.0.0", default-features = false, features = [
"bzip2",
"deflate",
"deflate64",
@@ -234,6 +246,7 @@ redundant_clone = "warn"
redundant_feature_names = "warn"
redundant_type_annotations = "warn"
todo = "warn"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
wildcard_dependencies = "warn"

View File

@@ -16,19 +16,13 @@
# About Project
## **AstralRinth • Empowering Your Minecraft Adventure**
## **AstralRinth • Empowering Your Minecraft Experience**
Welcome to **AstralRinth (AR)** — a powerful fork of Modrinth, reimagined to enhance your Minecraft journey. Whether you're a GUI enthusiast or a developer building with Modrinths API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
- *Recently, improved integration with the Git Astralium API has been added.*
**AstralRinth** — a powerful fork of Modrinth, reimagined to enhance your Minecraft journey. Whether you're a GUI enthusiast or a developer building with Modrinths API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
## **About the Software**
**AstralRinth** is a dedicated branch of the Theseus project, focused on **offline authentication**, offering you more flexibility and control. Play Minecraft without the need for constant online verification — a user-first approach to modern modded gaming.
## **AR • Unlocking Minecraft's Boundless Horizon**
This unique fork introduces a **free trial Minecraft experience**, bypassing license checks while maintaining rich functionality. Currently includes:
**AstralRinth** is a dedicated branch of the Modrinth (a.k.a Theseus) project, focused on **offline authentication**, offering you more flexibility and control. Play Minecraft without the need for constant online verification — a user-first approach to modern modded gaming.
---
@@ -43,8 +37,8 @@ To install the launcher:
| Extension | OS | Notes |
| --------- | ------- | --------------------------------------------------------------------- |
| `.msi` | Windows | Supported on all recent Windows versions |
| `.dmg` | macOS | Works on Ventura, Sonoma, Sequoia _(may also support older versions)_ |
| `.msi` | Windows | Supported on all recent Windows versions (10/11) |
| `.dmg` | macOS | Works on Ventura, Sonoma, Sequoia, Tahoe _(may also support older versions)_ |
| `.deb` | Linux | Basic support; compatibility may vary by distribution |
### Installation Warnings
@@ -70,7 +64,7 @@ Avoid using builds with these prefixes — they may be unstable or experimental:
- No ads in the entire launcher.
- Custom `.svg` vector icons for a distinct UI.
- Improved compatibility with both licensed and pirate accounts.
- Use **official microsoft accounts** or **offline/pirate accounts** — login won't break.
- Use **official microsoft accounts** or **offline/pirate accounts**.
- Supports license-free access for testing or personal use.
- No dependence on official authentication services.
- Discord Rich Presence integration:
@@ -82,7 +76,9 @@ Avoid using builds with these prefixes — they may be unstable or experimental:
- Built-in update alerts for new versions posted on Git Astralium.
- Automatic download and installation capabilities.
- Database migration fixes, when error occurred (Interactive Mode) (Modrinth issue)
- ElyBy skin system integration (AuthLib / Java)
- Ely.by full integration
- The official account skin system is managed by ely.by
- Offline accounts must install AuthLib through the instance settings
---
@@ -90,15 +86,15 @@ Avoid using builds with these prefixes — they may be unstable or experimental:
To begin using AstralRinth:
1. **Download Your OS Version**
1. **Download Latest Release**
- Go to the [releases page](https://git.astralium.su/didirus/AstralRinth/releases)
- [How to choose a file](#downloadable-file-extensions)
- [How to choose a release](#installation-warnings)
2. **Log In**
2. **Log in or create new offline account**
- Use your official Mojang/Microsoft account, or test using a non-licensed account.
- Use your official Microsoft account (MSA), or test using a non-licensed account (Offline).
3. **Launch Minecraft**
- Start Minecraft from the launcher.
@@ -119,4 +115,5 @@ To begin using AstralRinth:
If you'd like to support development, you can donate via the following crypto wallets:
- TON (Telegram): UQCG0a2fm5YzKzIm6E8M3_CqBQ5Q1RT95MU60c9UPjKn_NEN
- Toncoin (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk
- USDT (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk

View File

@@ -4,6 +4,7 @@ import {
ChangeSkinIcon,
CompassIcon,
DownloadIcon,
ExternalIcon,
HomeIcon,
LeftArrowIcon,
LibraryIcon,
@@ -18,6 +19,7 @@ import {
RestoreIcon,
RightArrowIcon,
SettingsIcon,
UserIcon,
WorldIcon,
XIcon,
} from '@modrinth/assets'
@@ -65,7 +67,7 @@ import { debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics
import { get_user } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { useFetch } from '@/helpers/fetch.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { list } from '@/helpers/profile.js'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
@@ -668,7 +670,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
:to="() => $refs.settingsModal.show()">
<SettingsIcon />
</NavButton>
</NavButton>
</template>
<template v-else>
<NavButton
@@ -677,29 +679,39 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
<SettingsIcon />
</NavButton>
</template>
<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>
<OverflowMenu
v-if="credentials"
v-tooltip.right="`Modrinth account`"
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast border-0 cursor-pointer"
:options="[
{
id: 'view-profile',
action: () => openUrl('https://modrinth.com/user/' + credentials.user.username),
},
{
id: 'sign-out',
action: () => logOut(),
color: 'danger',
},
]"
placement="right-end"
>
<Avatar :src="credentials.user.avatar_url" alt="" size="32px" circle />
<template #view-profile>
<UserIcon />
<span class="inline-flex items-center gap-1">
Signed in as
<span class="inline-flex items-center gap-1 text-contrast font-semibold">
<Avatar :src="credentials.user.avatar_url" alt="" size="20px" circle />
{{ credentials.user.username }}
</span>
</span>
<ExternalIcon />
</template>
<template #sign-out> <LogOutIcon /> Sign out </template>
</OverflowMenu>
<NavButton v-else v-tooltip.right="'Sign in to a Modrinth account'" :to="() => signIn()">
<LogInIcon class="text-brand" />
</NavButton>
</div>
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
@@ -837,20 +849,26 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
>
<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>
<div
class="p-4 pr-1 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
>
<h3 class="text-base text-primary font-medium 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">
<div class="py-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
<suspense>
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
<FriendsList
:credentials="credentials"
:sign-in="() => signIn()"
:refresh-credentials="fetchCredentials"
/>
</suspense>
</div>
<div v-if="news && news.length > 0" class="pt-4 flex flex-col items-center">
<h3 class="px-4 text-lg m-0 text-left w-full">News</h3>
<div class="px-4 pt-2 space-y-4 flex flex-col items-center w-full">
<div v-if="news && news.length > 0" class="p-4 pr-1 flex flex-col items-center">
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
<div class="space-y-4 flex flex-col items-center w-full">
<NewsArticleCard
v-for="(item, index) in news"
:key="`news-${index}`"
@@ -865,16 +883,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
</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>
<URLConfirmModal ref="urlModal" />

View File

@@ -24,7 +24,7 @@
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
const emit = defineEmits(['menu-closed', 'option-clicked'])
@@ -40,22 +40,27 @@ defineExpose({
item.value = passedItem
options.value = passedOptions
const menuWidth = contextMenu.value.clientWidth
const menuHeight = contextMenu.value.clientHeight
if (menuWidth + event.pageX >= window.innerWidth) {
left.value = event.pageX - menuWidth + 2 + 'px'
} else {
left.value = event.pageX - 2 + 'px'
}
if (menuHeight + event.pageY >= window.innerHeight) {
top.value = event.pageY - menuHeight + 2 + 'px'
} else {
top.value = event.pageY - 2 + 'px'
}
// show to get dimensions
shown.value = true
// then, adjust position if overflowing
nextTick(() => {
const menuWidth = contextMenu.value?.clientWidth || 200
const menuHeight = contextMenu.value?.clientHeight || 100
const minFromEdge = 10
if (event.pageX + menuWidth + minFromEdge >= window.innerWidth) {
left.value = Math.max(minFromEdge, event.pageX - menuWidth - minFromEdge) + 'px'
} else {
left.value = event.pageX + minFromEdge + 'px'
}
if (event.pageY + menuHeight + minFromEdge >= window.innerHeight) {
top.value = Math.max(minFromEdge, event.pageY - menuHeight - minFromEdge) + 'px'
} else {
top.value = event.pageY + minFromEdge + 'px'
}
})
},
})

View File

@@ -1,41 +1,41 @@
<script setup lang="ts">
import {
MailIcon,
MoreVerticalIcon,
SettingsIcon,
TrashIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets'
import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
commonMessages,
injectNotificationManager,
OverflowMenu,
useRelativeTime,
} from '@modrinth/ui'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { computed, onUnmounted, ref, watch } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get_user_many } from '@/helpers/cache'
import { friend_listener } from '@/helpers/events'
import { add_friend, friend_statuses, friends, remove_friend } from '@/helpers/friends'
import {
add_friend,
friends,
type FriendWithUserData,
remove_friend,
transformFriends,
} from '@/helpers/friends.ts'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const formatRelativeTime = useRelativeTime()
const props = defineProps<{
credentials: unknown | null
credentials: ModrinthCredentials | null
signIn: () => void
}>()
const userCredentials = computed(() => props.credentials)
const search = ref('')
const manageFriendsModal = ref()
const friendInvitesModal = ref()
const username = ref('')
@@ -47,61 +47,64 @@ async function addFriendFromModal() {
await loadFriends()
}
const friendOptions = ref()
async function handleFriendOptions(args) {
switch (args.option) {
case 'remove-friend':
await removeFriend(args.item)
break
async function addFriend(friend: FriendWithUserData) {
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
if (id) {
await add_friend(id).catch(handleError)
await loadFriends()
}
}
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: FriendWithUserData) {
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
if (id) {
await remove_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()
}
const userFriends = ref<FriendWithUserData[]>([])
const sortedFriends = computed<FriendWithUserData[]>(() =>
userFriends.value.slice().sort((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 filteredFriends = computed<FriendWithUserData[]>(() =>
sortedFriends.value.filter((x) =>
x.username.trim().toLowerCase().includes(search.value.trim().toLowerCase()),
),
)
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 activeFriends = computed<FriendWithUserData[]>(() =>
filteredFriends.value.filter((x) => !!x.status && x.online && x.accepted),
)
const onlineFriends = computed<FriendWithUserData[]>(() =>
filteredFriends.value.filter((x) => x.online && !x.status && x.accepted),
)
const offlineFriends = computed<FriendWithUserData[]>(() =>
filteredFriends.value.filter((x) => !x.online && x.accepted),
)
const pendingFriends = computed(() =>
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
filteredFriends.value
.filter((x) => !x.accepted && x.id !== userCredentials.value?.user_id)
.slice()
.sort((a, b) => b.created.diff(a.created)),
)
const incomingRequests = computed(() =>
userFriends.value
.filter((x) => !x.accepted && x.id === userCredentials.value?.user_id)
.slice()
.sort((a, b) => b.created.diff(a.created)),
)
const loading = ref(true)
@@ -110,34 +113,7 @@ async function loadFriends(timeout = false) {
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,
}
})
}
userFriends.value = await transformFriends(friendsList, userCredentials.value)
loading.value = false
} catch (e) {
console.error('Error loading friends', e)
@@ -152,6 +128,7 @@ watch(
() => {
if (userCredentials.value === undefined) {
userFriends.value = []
loading.value = false
} else if (userCredentials.value === null) {
userFriends.value = []
loading.value = false
@@ -166,49 +143,87 @@ const unlisten = await friend_listener(() => loadFriends())
onUnmounted(() => {
unlisten()
})
const messages = defineMessages({
addFriend: {
id: 'friends.action.add-friend',
defaultMessage: 'Add a friend',
},
addingAFriend: {
id: 'friends.add-friend.title',
defaultMessage: 'Adding a friend',
},
usernameTitle: {
id: 'friends.add-friend.username.title',
defaultMessage: "What's your friend's Modrinth username?",
},
usernameDescription: {
id: 'friends.add-friend.username.description',
defaultMessage: 'It may be different from their Minecraft username!',
},
usernamePlaceholder: {
id: 'friends.add-friend.username.placeholder',
defaultMessage: 'Enter Modrinth username...',
},
sendFriendRequest: {
id: 'friends.add-friend.submit',
defaultMessage: 'Send friend request',
},
viewFriendRequests: {
id: 'friends.action.view-friend-requests',
defaultMessage: '{count} friend {count, plural, one {request} other {requests}}',
},
searchFriends: {
id: 'friends.search-friends-placeholder',
defaultMessage: 'Search friends...',
},
friends: {
id: 'friends.heading',
defaultMessage: 'Friends',
},
pending: {
id: 'friends.heading.pending',
defaultMessage: 'Pending',
},
active: {
id: 'friends.heading.active',
defaultMessage: 'Active',
},
online: {
id: 'friends.heading.online',
defaultMessage: 'Online',
},
offline: {
id: 'friends.heading.offline',
defaultMessage: 'Offline',
},
noFriendsMatch: {
id: 'friends.no-friends-match',
defaultMessage: `No friends matching ''{query}''`,
},
signInToAddFriends: {
id: 'friends.sign-in-to-add-friends',
defaultMessage:
"<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!",
},
addFriendsToShare: {
id: 'friends.add-friends-to-share',
defaultMessage: "<link>Add friends</link> to see what they're playing!",
},
})
</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">
<p v-if="incomingRequests.length === 0">You have no pending friend requests :C</p>
<div v-else class="flex flex-col gap-4 min-w-[40rem]">
<div v-for="friend in incomingRequests" :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 class="grid grid-cols-[1fr_auto] w-full gap-4">
<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 v-if="friend.id === userCredentials?.user_id">
<span class="text-contrast">{{ friend.username }}</span> sent you a friend request
</template>
<template v-else>
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
@@ -219,7 +234,7 @@ onUnmounted(() => {
</p>
</div>
<div class="flex gap-2">
<template v-if="friend.id === userCredentials.user_id">
<template v-if="friend.id === userCredentials?.user_id">
<ButtonStyled color="brand">
<button @click="addFriend(friend)">
<UserPlusIcon />
@@ -246,78 +261,89 @@ onUnmounted(() => {
</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..."
@keyup.enter="addFriendFromModal"
/>
<ModalWrapper ref="addFriendModal" :header="formatMessage(messages.addingAFriend)">
<div class="min-w-[30rem]">
<h2 class="m-0 text-base font-medium text-primary">
{{ formatMessage(messages.usernameTitle) }}
</h2>
<p class="m-0 mt-1 text-sm text-secondary leading-tight">
{{ formatMessage(messages.usernameDescription) }}
</p>
<div class="flex items-center gap-2 mt-4">
<div class="iconified-input flex-1">
<UserIcon aria-hidden="true" />
<input
v-model="username"
type="text"
:placeholder="formatMessage(messages.usernamePlaceholder)"
@keyup.enter="addFriendFromModal"
/>
</div>
<ButtonStyled color="brand">
<button :disabled="username.length === 0" @click="addFriendFromModal">
<SendIcon />
{{ formatMessage(messages.sendFriendRequest) }}
</button>
</ButtonStyled>
</div>
</div>
<ButtonStyled color="brand">
<button :disabled="username.length === 0" @click="addFriendFromModal">
<UserPlusIcon />
Add friend
</ModalWrapper>
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 ml-2 mr-1">
<template v-if="sortedFriends.length > 0">
<ButtonStyled circular type="transparent">
<button
v-tooltip="formatMessage(messages.addFriend)"
:aria-label="formatMessage(messages.addFriend)"
@click="addFriendModal.show"
>
<UserPlusIcon />
</button>
</ButtonStyled>
<div class="iconified-input flex-1">
<input
v-model="search"
type="text"
class="friends-search-bar flex w-full"
:placeholder="formatMessage(messages.searchFriends)"
@keyup.esc="search = ''"
/>
<button
v-if="search"
v-tooltip="formatMessage(commonMessages.clearButton)"
class="r-btn flex items-center justify-center bg-transparent button-animation p-2 cursor-pointer appearance-none border-none"
@click="search = ''"
>
<XIcon />
</button>
</div>
</template>
<h3 v-else class="ml-2 w-full text-base text-primary font-medium m-0">
{{ formatMessage(messages.friends) }}
</h3>
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
<button
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
class="relative"
:aria-label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
@click="friendInvitesModal.show"
>
<MailIcon />
<span
v-if="incomingRequests.length > 0"
aria-hidden="true"
class="absolute bg-brand text-brand-inverted text-[8px] top-0.5 px-1 right-0.5 min-w-3 h-3 rounded-full flex items-center justify-center font-bold"
>
{{ incomingRequests.length }}
</span>
</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">
<div class="flex flex-col gap-3">
<h3 v-if="loading" class="ml-4 mr-1 text-base text-primary font-medium m-0">
{{ formatMessage(messages.friends) }}
</h3>
<template v-if="loading">
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse ml-4 mr-1">
<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>
@@ -325,50 +351,77 @@ onUnmounted(() => {
</div>
</div>
</template>
<template v-else-if="acceptedFriends.length === 0">
<div class="text-sm">
<template v-else-if="sortedFriends.length === 0">
<div class="text-sm ml-4 mr-1">
<div v-if="!userCredentials">
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
<IntlFormatted :message-id="messages.signInToAddFriends">
<template #link="{ children }">
<span class="font-semibold text-brand cursor-pointer" @click="signIn">
<component :is="() => children" />
</span>
</template>
</IntlFormatted>
</div>
<div v-else>
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
to share what you're playing!
<IntlFormatted :message-id="messages.addFriendsToShare">
<template #link="{ children }">
<span class="font-semibold text-brand cursor-pointer" @click="addFriendModal.show">
<component :is="() => children" />
</span>
</template>
</IntlFormatted>
</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>
<FriendsSection
v-if="activeFriends.length > 0"
:is-searching="!!search"
open-by-default
:friends="activeFriends"
:heading="formatMessage(messages.active)"
:remove-friend="removeFriend"
/>
<FriendsSection
v-if="onlineFriends.length > 0"
:is-searching="!!search"
open-by-default
:friends="onlineFriends"
:heading="formatMessage(messages.online)"
:remove-friend="removeFriend"
/>
<FriendsSection
v-if="offlineFriends.length > 0"
:is-searching="!!search"
:open-by-default="activeFriends.length + onlineFriends.length < 3"
:friends="offlineFriends"
:heading="formatMessage(messages.offline)"
:remove-friend="removeFriend"
/>
<FriendsSection
v-if="pendingFriends.length > 0"
:is-searching="!!search"
open-by-default
:friends="pendingFriends"
:heading="formatMessage(messages.pending)"
:remove-friend="removeFriend"
/>
<p v-if="filteredFriends.length === 0 && search" class="text-sm text-secondary my-1 mx-4">
{{ formatMessage(messages.noFriendsMatch, { query: search }) }}
</p>
</template>
</div>
</template>
<style scoped>
.friends-search-bar {
background: none;
border: 2px solid var(--color-button-bg) !important;
padding: 8px;
border-radius: 12px;
height: 36px;
}
.friends-search-bar::placeholder {
@apply text-sm font-normal;
}
</style>

View File

@@ -0,0 +1,185 @@
<script setup lang="ts">
import { MoreVerticalIcon, TrashIcon, UserIcon, XIcon } from '@modrinth/assets'
import { Accordion, Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { useTemplateRef } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import type { FriendWithUserData } from '@/helpers/friends.ts'
const { formatMessage } = useVIntl()
const props = withDefaults(
defineProps<{
friends: FriendWithUserData[]
heading: string
removeFriend: (friend: FriendWithUserData) => Promise<void>
isSearching?: boolean
openByDefault?: boolean
}>(),
{
isSearching: false,
openByDefault: false,
},
)
function createContextMenuOptions(friend: FriendWithUserData) {
if (friend.accepted) {
return [
{
name: 'view-profile',
},
{
name: 'remove-friend',
color: 'danger',
},
]
} else {
return [
{
name: 'view-profile',
},
{
name: 'cancel-request',
},
]
}
}
function openProfile(username: string) {
openUrl('https://modrinth.com/user/' + username)
}
const friendOptions = useTemplateRef('friendOptions')
async function handleFriendOptions(args: { item: FriendWithUserData; option: string }) {
switch (args.option) {
case 'remove-friend':
case 'cancel-request':
await props.removeFriend(args.item)
break
case 'view-profile':
openProfile(args.item.username)
}
}
const messages = defineMessages({
removeFriend: {
id: 'friends.friend.remove-friend',
defaultMessage: 'Remove friend',
},
heading: {
id: 'friends.section.heading',
defaultMessage: '{title} - {count}',
},
friendRequestSent: {
id: 'friends.friend.request-sent',
defaultMessage: 'Friend request sent',
},
cancelRequest: {
id: 'friends.friend.cancel-request',
defaultMessage: 'Cancel request',
},
viewProfile: {
id: 'friends.friend.view-profile',
defaultMessage: 'View profile',
},
})
</script>
<template>
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
<template #view-profile>
<UserIcon />
{{ formatMessage(messages.viewProfile) }}
</template>
<template #remove-friend> <TrashIcon /> {{ formatMessage(messages.removeFriend) }} </template>
<template #cancel-request> <XIcon /> {{ formatMessage(messages.cancelRequest) }} </template>
</ContextMenu>
<Accordion
:open-by-default="openByDefault"
:force-open="isSearching"
:button-class="
'pl-4 pr-3 flex w-full items-center bg-transparent border-0 p-0' +
(isSearching
? ''
: ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all')
"
>
<template #title>
<h3 class="text-base text-primary font-medium m-0">
{{ formatMessage(messages.heading, { title: heading, count: friends.length }) }}
</h3>
</template>
<template #default>
<div class="pt-3 flex flex-col gap-1">
<div
v-for="friend in friends"
:key="friend.username"
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full ml-4 mr-1"
@contextmenu.prevent.stop="
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
"
>
<div class="relative">
<Avatar
:src="friend.avatar"
:class="{ grayscale: !friend.online && friend.accepted }"
class="w-12 h-12 rounded-full"
size="32px"
circle
/>
<span
v-if="friend.online"
aria-hidden="true"
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
/>
</div>
<div class="flex flex-col">
<span
class="text-sm m-0"
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
>
{{ friend.username }}
</span>
<span v-if="!friend.accepted" class="m-0 text-xs">
{{ formatMessage(messages.friendRequestSent) }}
</span>
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
</div>
<ButtonStyled v-if="friend.accepted" circular type="transparent">
<OverflowMenu
class="opacity-0 group-hover:opacity-100 transition-opacity"
:options="[
{
id: 'view-profile',
action: () => openProfile(friend.username),
},
{
id: 'remove-friend',
action: () => removeFriend(friend),
color: 'red',
},
]"
>
<MoreVerticalIcon />
<template #view-profile>
<UserIcon />
{{ formatMessage(messages.viewProfile) }}
</template>
<template #remove-friend>
<TrashIcon />
{{ formatMessage(messages.removeFriend) }}
</template>
</OverflowMenu>
</ButtonStyled>
<ButtonStyled v-else type="transparent" circular>
<button v-tooltip="formatMessage(messages.cancelRequest)" @click="removeFriend(friend)">
<XIcon />
</button>
</ButtonStyled>
</div>
</div>
</template>
</Accordion>
</template>

View File

@@ -211,28 +211,34 @@ const messages = defineMessages({
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
<div class="space-y-4">
<div class="space-y-2">
<p>The new version of the AstralRinth launcher is available.</p>
<strong>The new version of the AstralRinth launcher is available!</strong>
<p>Your version is outdated. We recommend that you update to the latest version.</p>
<p><strong> Warning </strong></p>
<br/>
<br/>
<p><strong> Please, read this notice before initialize update process</strong></p>
<p>
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
your files, so you should always make copies of them and keep them in a safe place.
Before updating, make sure that you have saved and closed all running instances and made a backup copy of the launcher data such as
<code>%appdata%\Roaming\AstralRinthApp</code> on Windows or <code>~/Library/Application Support/AstralRinthApp</code> on macOS.
Remember that the authors of the product are not responsible for the breakdown of
your files, so you should always make back up copies of them and keep them in a safe place.
</p>
</div>
<div class="text-sm text-secondary space-y-1">
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank"
rel="noopener noreferrer"><strong>Source:</strong> Git Astralium</a>
<p>
<strong>Version on remote server:</strong>
<span id="releaseData" class="neon-text"></span>
</p>
<p>
<strong>Version on local device:</strong>
<strong> Latest release tag:</strong>
<span id="releaseTag" class="neon-text"></span>
<br/>
<strong> Latest release title:</strong>
<span id="releaseTitle" class="neon-text"></span>
<br/>
<strong>💾 Installed & Running version:</strong>
<span class="neon-text">v{{ version }}</span>
</p>
</div>
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank"
rel="noopener noreferrer">
Checkout our git repository
</a>
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
<Button class="bordered" @click="updateModalView.hide()">Cancel</Button>
<Button class="bordered" @click="initDownload()">Download file</Button>
@@ -271,4 +277,11 @@ const messages = defineMessages({
@import '../../../../../../packages/assets/styles/neon-icon.scss';
@import '../../../../../../packages/assets/styles/neon-button.scss';
@import '../../../../../../packages/assets/styles/neon-text.scss';
code {
background: linear-gradient(90deg, #005eff, #00cfff);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
</style>

View File

@@ -130,6 +130,7 @@ import {
remove_custom_skin,
type Skin,
type SkinModel,
type SkinTextureUrl,
unequip_skin,
} from '@/helpers/skins.ts'
@@ -142,7 +143,7 @@ const currentSkin = ref<Skin | null>(null)
const shouldRestoreModal = ref(false)
const isSaving = ref(false)
const uploadedTextureUrl = ref<string | null>(null)
const uploadedTextureUrl = ref<SkinTextureUrl | null>(null)
const previewSkin = ref<string>('')
const variant = ref<SkinModel>('CLASSIC')
@@ -188,7 +189,7 @@ function getSortedCapeExcluding(excludeId: string): Cape | undefined {
async function loadPreviewSkin() {
if (uploadedTextureUrl.value) {
previewSkin.value = uploadedTextureUrl.value
previewSkin.value = uploadedTextureUrl.value.normalized
} else if (currentSkin.value) {
try {
previewSkin.value = await get_normalized_skin_texture(currentSkin.value)
@@ -253,11 +254,11 @@ async function show(e: MouseEvent, skin?: Skin) {
modal.value?.show(e)
}
async function showNew(e: MouseEvent, skinTextureUrl: string) {
async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
mode.value = 'new'
currentSkin.value = null
uploadedTextureUrl.value = skinTextureUrl
variant.value = await determineModelType(skinTextureUrl)
variant.value = await determineModelType(skinTextureUrl.original)
selectedCape.value = undefined
visibleCapeList.value = []
initVisibleCapeList()
@@ -267,7 +268,7 @@ async function showNew(e: MouseEvent, skinTextureUrl: string) {
modal.value?.show(e)
}
async function restoreWithNewTexture(skinTextureUrl: string) {
async function restoreWithNewTexture(skinTextureUrl: SkinTextureUrl) {
uploadedTextureUrl.value = skinTextureUrl
await loadPreviewSkin()
@@ -361,7 +362,7 @@ async function save() {
let textureUrl: string
if (uploadedTextureUrl.value) {
textureUrl = uploadedTextureUrl.value
textureUrl = uploadedTextureUrl.value.original
} else {
textureUrl = currentSkin.value!.texture
}

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

@@ -0,0 +1,79 @@
import type { User } from '@modrinth/utils'
import { invoke } from '@tauri-apps/api/core'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { get_user_many } from '@/helpers/cache'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
export type UserStatus = {
user_id: string
profile_name: string | null
last_update: string
}
export type UserFriend = {
id: string
friend_id: string
accepted: boolean
created: string
}
export async function friends(): Promise<UserFriend[]> {
return await invoke('plugin:friends|friends')
}
export async function friend_statuses(): Promise<UserStatus[]> {
return await invoke('plugin:friends|friend_statuses')
}
export async function add_friend(userId: string): Promise<void> {
return await invoke('plugin:friends|add_friend', { userId })
}
export async function remove_friend(userId: string): Promise<void> {
return await invoke('plugin:friends|remove_friend', { userId })
}
export type FriendWithUserData = {
id: string
friend_id: string | null
status: string | null
last_updated: Dayjs | null
created: Dayjs
username: string
accepted: boolean
online: boolean
avatar: string
}
export async function transformFriends(
friends: UserFriend[],
credentials: ModrinthCredentials | null,
): Promise<FriendWithUserData[]> {
if (friends.length === 0 || !credentials) {
return []
}
const friendStatuses = await friend_statuses()
const users = await get_user_many(
friends.map((x) => (x.id === credentials.user_id ? x.friend_id : x.id)),
)
return friends.map((friend) => {
const user = users.find((x: User) => 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 ?? null,
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,
}
})
}

View File

@@ -5,18 +5,25 @@
*/
import { invoke } from '@tauri-apps/api/core'
export async function login() {
export type ModrinthCredentials = {
session: string
expires: string
user_id: string
active: boolean
}
export async function login(): Promise<ModrinthCredentials> {
return await invoke('plugin:mr-auth|modrinth_login')
}
export async function logout() {
export async function logout(): Promise<void> {
return await invoke('plugin:mr-auth|logout')
}
export async function get() {
export async function get(): Promise<ModrinthCredentials | null> {
return await invoke('plugin:mr-auth|get')
}
export async function cancelLogin() {
export async function cancelLogin(): Promise<void> {
return await invoke('plugin:mr-auth|cancel_modrinth_login')
}

View File

@@ -67,6 +67,8 @@ export type AppSettings = {
skipped_update: string | null
pending_update_toast_for_version: string | null
auto_download_updates: boolean | null
version: number
}
// Get full settings object

View File

@@ -22,6 +22,11 @@ export interface Skin {
is_equipped: boolean
}
export interface SkinTextureUrl {
original: string
normalized: string
}
export const DEFAULT_MODEL_SORTING = ['Steve', 'Alex'] as string[]
export const DEFAULT_MODELS: Record<string, SkinModel> = {

View File

@@ -1,96 +1,98 @@
import { ref } from 'vue'
import { getVersion } from '@tauri-apps/api/app'
import { initUpdateLauncher, getOS } from '@/helpers/utils.js'
import { ref } from 'vue'
import { getOS, initUpdateLauncher } from '@/helpers/utils.js'
export const allowState = ref(false)
export const installState = ref(false)
export const updateState = ref(false)
const currentOS = ref('')
const releaseLink = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/releases/latest`
const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`]
const api = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/releases/latest`
const osList = ['macos', 'windows', 'linux']
const macExtensionList = ['.dmg', '.pkg']
const windowsExtensionList = ['.exe', '.msi']
const systems = ['macos', 'windows', 'linux']
const macosExtensions = ['.dmg', '.pkg', '.app']
const windowsExtensions = ['.exe', '.msi']
const blacklistPrefixes = [
`dev`,
`nightly`,
`dirty`,
`dirty-dev`,
`dirty-nightly`,
`dirty_dev`,
`dirty_nightly`,
const blacklistBeginPrefixes = [
`dev`,
`nightly`,
`dirty`,
`dirty-dev`,
`dirty-nightly`,
`dirty_dev`,
`dirty_nightly`,
] // This is blacklisted builds for download. For example, file.startsWith('dev') is not allowed.
export async function getRemote(isDownloadState) {
var releaseData = null;
var result = false;
try {
const response = await fetch(releaseLink);
if (!response.ok) {
throw new Error(response.status);
}
const remoteData = await response.json();
currentOS.value = await getOS();
const remoteLatestReleaseTag = remoteData.tag_name;
releaseData = document.getElementById('releaseData');
const remoteVersion = releaseData ? (releaseData.textContent = remoteLatestReleaseTag) : remoteLatestReleaseTag;
if (osList.includes(currentOS.value.toLowerCase())) {
const localVersion = await getVersion();
const isUpdateAvailable = !remoteVersion.includes(localVersion);
updateState.value = isUpdateAvailable;
allowState.value = isUpdateAvailable;
} else {
updateState.value = false;
allowState.value = false;
}
if (isDownloadState) {
installState.value = true;
const builds = remoteData.assets;
const fileName = getInstaller(getExtension(), builds);
result = fileName ? await initUpdateLauncher(fileName[1], fileName[0], currentOS.value, true) : false;
installState.value = false;
}
console.log('Update available state is', updateState.value);
console.log('Remote version is', remoteVersion);
console.log('Local version is', await getVersion());
console.log('Operating System is', currentOS.value);
return result;
} catch (error) {
console.error(failedFetch[0], error);
if (!releaseData) {
const errorData = document.getElementById('releaseData');
if (errorData) {
errorData.textContent = `${error.message}`;
}
updateState.value = false;
allowState.value = false;
installState.value = false;
}
}
var releaseTag = null;
var releaseTitle = null;
var result = false;
currentOS.value = await getOS();
try {
const response = await fetch(api);
if (!response.ok) {
throw new Error(response.status);
}
const remoteData = await response.json();
releaseTag = document.getElementById('releaseTag');
releaseTitle = document.getElementById('releaseTitle');
if (releaseTag && releaseTitle) {
releaseTag.textContent = remoteData.tag_name;
releaseTitle.textContent = remoteData.name;
}
if (systems.includes(currentOS.value.toLowerCase())) {
const localVersion = await getVersion();
const isUpdateAvailable = !remoteData.tag_name.includes(localVersion);
updateState.value = isUpdateAvailable;
allowState.value = isUpdateAvailable;
} else {
updateState.value = false;
allowState.value = false;
}
if (isDownloadState) {
try {
installState.value = true;
const builds = remoteData.assets;
const fileName = getInstaller(getExtension(), builds);
result = fileName ? await initUpdateLauncher(fileName[1], fileName[0], currentOS.value, true) : false;
installState.value = false;
} catch (err) {
installState.value = false;
}
}
console.log('Update available state is', updateState.value);
console.log('Remote version is', remoteData.tag_name);
console.log('Remote title is', remoteData.name);
console.log('Local version is', await getVersion());
console.log('Operating System is', currentOS.value);
return result;
} catch (error) {
console.error("Failed to fetch remote releases:", error);
if (!releaseTag) {
updateState.value = false;
allowState.value = false;
installState.value = false;
}
}
}
function getInstaller(osExtension, builds) {
console.log(osExtension, builds)
for (const build of builds) {
if (blacklistPrefixes.some(prefix => build.name.startsWith(prefix))) {
continue;
}
if (osExtension.some(ext => build.name.endsWith(ext))) {
console.log(build.name, build.browser_download_url);
return [build.name, build.browser_download_url];
}
}
return null;
console.log(osExtension, builds)
for (const build of builds) {
if (blacklistBeginPrefixes.some(prefix => build.name.startsWith(prefix))) {
continue;
}
if (osExtension.some(ext => build.name.endsWith(ext))) {
console.log(build.name, build.browser_download_url);
return [build.name, build.browser_download_url];
}
}
return null;
}
function getExtension() {
return osList.find(osName => osName === currentOS.value.toLowerCase())?.endsWith('macos')
? macExtensionList
: windowsExtensionList;
return systems.find(osName => osName === currentOS.value.toLowerCase())?.endsWith('macos')
? macosExtensions
: windowsExtensions;
}

View File

@@ -65,6 +65,69 @@
"app.update.reload-to-update": {
"message": "Reload to install update"
},
"friends.action.add-friend": {
"message": "Add a friend"
},
"friends.action.view-friend-requests": {
"message": "{count} friend {count, plural, one {request} other {requests}}"
},
"friends.add-friend.submit": {
"message": "Send friend request"
},
"friends.add-friend.title": {
"message": "Adding a friend"
},
"friends.add-friend.username.description": {
"message": "It may be different from their Minecraft username!"
},
"friends.add-friend.username.placeholder": {
"message": "Enter Modrinth username..."
},
"friends.add-friend.username.title": {
"message": "What's your friend's Modrinth username?"
},
"friends.add-friends-to-share": {
"message": "<link>Add friends</link> to see what they're playing!"
},
"friends.friend.cancel-request": {
"message": "Cancel request"
},
"friends.friend.remove-friend": {
"message": "Remove friend"
},
"friends.friend.request-sent": {
"message": "Friend request sent"
},
"friends.friend.view-profile": {
"message": "View profile"
},
"friends.heading": {
"message": "Friends"
},
"friends.heading.active": {
"message": "Active"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Pending"
},
"friends.no-friends-match": {
"message": "No friends matching ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Search friends..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!"
},
"instance.add-server.add-and-play": {
"message": "Add and play"
},

View File

@@ -31,7 +31,7 @@ import { get_default_user, login as login_flow, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
import { get as getSettings } from '@/helpers/settings.ts'
import type { Cape, Skin } from '@/helpers/skins.ts'
import type { Cape, Skin, SkinTextureUrl } from '@/helpers/skins.ts'
import {
equip_skin,
filterDefaultSkins,
@@ -245,16 +245,18 @@ function openUploadSkinModal(e: MouseEvent) {
function onSkinFileUploaded(buffer: ArrayBuffer) {
const fakeEvent = new MouseEvent('click')
normalize_skin_texture(`data:image/png;base64,` + arrayBufferToBase64(buffer)).then(
(skinTextureNormalized: Uint8Array) => {
const skinTexUrl = `data:image/png;base64,` + arrayBufferToBase64(skinTextureNormalized)
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
editSkinModal.value.restoreWithNewTexture(skinTexUrl)
} else {
editSkinModal.value?.showNew(fakeEvent, skinTexUrl)
}
},
)
const originalSkinTexUrl = `data:image/png;base64,` + arrayBufferToBase64(buffer)
normalize_skin_texture(originalSkinTexUrl).then((skinTextureNormalized: Uint8Array) => {
const skinTexUrl: SkinTextureUrl = {
original: originalSkinTexUrl,
normalized: `data:image/png;base64,` + arrayBufferToBase64(skinTextureNormalized),
}
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
editSkinModal.value.restoreWithNewTexture(skinTexUrl)
} else {
editSkinModal.value?.showNew(fakeEvent, skinTexUrl)
}
})
}
function onUploadCanceled() {

View File

@@ -139,9 +139,9 @@
<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 #open_folder> <FolderOpenIcon /> Open folder </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
<template #open_link> <ClipboardCopyIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #open_link> <GlobeIcon /> 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>

View File

@@ -92,7 +92,7 @@ import {
XIcon,
} from '@modrinth/assets'
import { Button, Card } from '@modrinth/ui'
import { ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
// import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
import { trackEvent } from '@/helpers/analytics'
@@ -151,17 +151,26 @@ const expandImage = (item, index) => {
function keyListener(e) {
if (expandedGalleryItem.value) {
e.preventDefault()
if (e.key === 'Escape') {
e.preventDefault()
hideImage()
} else if (e.key === 'ArrowLeft') {
e.preventDefault()
previousImage()
} else if (e.key === 'ArrowRight') {
e.preventDefault()
nextImage()
}
}
}
document.addEventListener('keypress', keyListener)
onMounted(() => {
document.addEventListener('keydown', keyListener)
})
onUnmounted(() => {
document.removeEventListener('keydown', keyListener)
})
</script>
<style scoped lang="scss">

View File

@@ -148,9 +148,15 @@ fn main() {
} else {
"app-window-state.json".to_string()
},
)
// Use *only* POSITION and SIZE state flags, because saving VISIBLE causes the `visible: false` to not take effect
.with_state_flags(
tauri_plugin_window_state::StateFlags::POSITION
| tauri_plugin_window_state::StateFlags::SIZE
| tauri_plugin_window_state::StateFlags::MAXIMIZED,
)
.build(),
)
)
.setup(|app| {
#[cfg(target_os = "macos")]
{

View File

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

View File

@@ -1,3 +1,3 @@
{
"mainBinaryName": "ModrinthApp"
"mainBinaryName": "AstralRinthApp"
}

View File

@@ -7,7 +7,7 @@
"fullscreen": false,
"height": 800,
"resizable": true,
"title": "Modrinth App",
"title": "AstralRinth App",
"width": 1280,
"minHeight": 700,
"minWidth": 1100,

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
FROM rust:1.89.0 AS build
FROM rust:1.90.0 AS build
WORKDIR /usr/src/daedalus
COPY . .
@@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/usr/src/daedalus/target \
mkdir /daedalus \
&& cp /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
FROM debian:bookworm-slim
FROM debian:trixie-slim
LABEL org.opencontainers.image.source=https://github.com/modrinth/code
LABEL org.opencontainers.image.title=daedalus

View File

@@ -2762,10 +2762,7 @@
},
{
"_comment": "Replace glfw from 3.3.1 with version from 3.3.2 to prevent stack smashing",
"match": [
"org.lwjgl:lwjgl-glfw-natives-linux:3.3.1",
"org.lwjgl:lwjgl-glfw:3.3.1:natives-linux"
],
"match": ["org.lwjgl:lwjgl-glfw-natives-linux:3.3.1"],
"override": {
"downloads": {
"artifact": {
@@ -2776,5 +2773,115 @@
},
"name": "org.lwjgl:lwjgl-glfw-natives-linux:3.3.2-lwjgl.1"
}
},
{
"_comment": "Use newer JNA on macOS to prevent crashes due to faulty assertion",
"match": [
"net.java.dev.jna:jna:5.6.0",
"net.java.dev.jna:jna:5.8.0",
"net.java.dev.jna:jna:5.9.0",
"net.java.dev.jna:jna:5.10.0",
"net.java.dev.jna:jna:5.12.1"
],
"override": {
"rules": [
{
"action": "allow"
},
{
"action": "disallow",
"os": {
"name": "osx"
}
},
{
"action": "disallow",
"os": {
"name": "osx-arm64"
}
}
]
},
"additionalLibraries": [
{
"downloads": {
"artifact": {
"sha1": "1200e7ebeedbe0d10062093f32925a912020e747",
"size": 1879325,
"url": "https://libraries.minecraft.net/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar"
}
},
"name": "net.java.dev.jna:jna:5.13.0",
"rules": [
{
"action": "allow",
"os": {
"name": "osx"
}
},
{
"action": "allow",
"os": {
"name": "osx-arm64"
}
}
]
}
]
},
{
"_comment": "Use newer JNA on macOS to prevent crashes due to faulty assertion",
"match": [
"net.java.dev.jna:jna-platform:5.6.0",
"net.java.dev.jna:jna-platform:5.8.0",
"net.java.dev.jna:jna-platform:5.9.0",
"net.java.dev.jna:jna-platform:5.10.0",
"net.java.dev.jna:jna-platform:5.12.1"
],
"override": {
"rules": [
{
"action": "allow"
},
{
"action": "disallow",
"os": {
"name": "osx"
}
},
{
"action": "disallow",
"os": {
"name": "osx-arm64"
}
}
]
},
"additionalLibraries": [
{
"downloads": {
"artifact": {
"sha1": "88e9a306715e9379f3122415ef4ae759a352640d",
"size": 1363209,
"url": "https://libraries.minecraft.net/net/java/dev/jna/jna-platform/5.13.0/jna-platform-5.13.0.jar"
}
},
"name": "net.java.dev.jna:jna-platform:5.13.0",
"rules": [
{
"action": "allow",
"os": {
"name": "osx"
}
},
{
"action": "allow",
"os": {
"name": "osx-arm64"
}
}
]
}
]
}
]

View File

@@ -186,6 +186,8 @@ pub struct LibraryPatch {
}
fn fetch_library_patches() -> Result<Vec<LibraryPatch>, Error> {
// The file below is a copy of https://github.com/PrismLauncher/meta/blob/main/meta/common/mojang-library-patches.json.
// That file belongs to a repository licensed under the Microsoft Public License (Ms-PL)
let patches = include_bytes!("../library-patches.json");
Ok(serde_json::from_slice(patches)?)
}

View File

@@ -5,7 +5,7 @@ description: Guide for contributing to Modrinth's backend
This project is part of our [monorepo](https://github.com/modrinth/code). You can find it in the `apps/labrinth` directory. The instructions below assume that you have switched your working directory to the `apps/labrinth` subdirectory.
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432, a MeiliSearch instance on port 7700, and a [Mailpit](https://mailpit.axllent.org/) SMTP server on port 1025, with a web UI to inspect sent emails on port 8025. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), cmake, and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432, a MeiliSearch instance on port 7700, and a [Mailpit](https://mailpit.axllent.org/) SMTP server on port 1025, with a web UI to inspect sent emails on port 8025. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
To get a basic configuration, copy the `.env.local` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:

View File

@@ -2,9 +2,8 @@
::backdrop,
:root[data-theme='light'],
[data-theme='light'] ::backdrop {
--sl-font-system:
Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell,
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
--sl-font-system: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
--sl-color-white: var(--color-contrast); /* “white” */
--sl-color-gray-1: var(--color-base);

View File

@@ -1,20 +1,23 @@
<template>
<div class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised">
<nuxt-link
:to="flags.enableMedalPromotion ? '/servers?plan&ref=medal' : '/servers'"
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
<div class="wrapper relative mb-3 flex w-full justify-center rounded-2xl">
<AutoLink
:to="currentAd.link"
:aria-label="currentAd.description"
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit] bg-bg-raised"
>
<img
:src="`https://cdn-raw.modrinth.com/${flags.enableMedalPromotion ? 'medal-modrinth-servers' : 'modrinth-servers-placeholder'}-light.webp`"
alt="Host your next server with Modrinth Servers"
:src="currentAd.light"
aria-hidden="true"
:alt="currentAd.description"
class="light-image hidden rounded-[inherit]"
/>
<img
:src="`https://cdn-raw.modrinth.com/${flags.enableMedalPromotion ? 'medal-modrinth-servers' : 'modrinth-servers-placeholder'}-dark.webp`"
alt="Host your next server with Modrinth Servers"
:src="currentAd.dark"
aria-hidden="true"
:alt="currentAd.description"
class="dark-image rounded-[inherit]"
/>
</nuxt-link>
</AutoLink>
<div
class="absolute top-0 flex items-center justify-center overflow-hidden rounded-2xl bg-bg-raised"
>
@@ -23,6 +26,8 @@
</div>
</template>
<script setup>
import { AutoLink } from '@modrinth/ui'
const flags = useFeatureFlags()
useHead({
@@ -55,6 +60,25 @@ useHead({
],
})
const AD_PRESETS = {
medal: {
light: 'https://cdn-raw.modrinth.com/medal-modrinth-servers-light-new.webp',
dark: 'https://cdn-raw.modrinth.com/medal-modrinth-servers-dark-new.webp',
description: 'Host your next server with Modrinth Servers',
link: '/servers?plan&ref=medal',
},
'modrinth-servers': {
light: 'https://cdn-raw.modrinth.com/modrinth-servers-placeholder-light.webp',
dark: 'https://cdn-raw.modrinth.com/modrinth-servers-placeholder-dark.webp',
description: 'Host your next server with Modrinth Servers',
link: '/servers',
},
}
const currentAd = computed(() =>
flags.value.enableMedalPromotion ? AD_PRESETS.medal : AD_PRESETS['modrinth-servers'],
)
onMounted(() => {
window.tude = window.tude || { cmd: [] }
window.Raven = window.Raven || { cmd: [] }
@@ -137,10 +161,14 @@ iframe[id^='google_ads_iframe'] {
}
@media (max-width: 1024px) {
.ad-parent {
.wrapper {
display: none;
}
}
.wrapper > * {
box-shadow: var(--shadow-card);
}
</style>
<style lang="scss" scoped>

View File

@@ -8,6 +8,11 @@
{{ analytics.error.value }}
</div>
</div>
<div v-else-if="!isInitialized || analytics.loading.value" class="universal-card">
<h2>
<span class="label__title">Loading analytics...</span>
</h2>
</div>
<div v-else class="graphs">
<div class="graphs__vertical-bar">
<client-only>
@@ -419,6 +424,7 @@ const isUsingProjectColors = computed({
const startDate = ref(dayjs().startOf('day'))
const endDate = ref(dayjs().endOf('day'))
const timeResolution = ref(30)
const isInitialized = ref(false)
onBeforeMount(() => {
// Load cached data and range from localStorage - cache.
@@ -449,6 +455,8 @@ onMounted(() => {
startDate.value = ranges.startDate
endDate.value = ranges.endDate
timeResolution.value = selectedRange.value.timeResolution
isInitialized.value = true
})
const internalRange: Ref<RangeObject> = ref(null as unknown as RangeObject)
@@ -482,6 +490,7 @@ const analytics = useFetchAllAnalytics(
startDate,
endDate,
timeResolution,
isInitialized,
)
const formattedCategorySubtitle = computed(() => {

View File

@@ -631,6 +631,7 @@ onMounted(() => {
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeybinds)
notifications.setNotificationLocation('right')
})

View File

@@ -13,14 +13,23 @@ import {
TrashIcon,
XIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from '@modrinth/ui'
import {
ButtonStyled,
commonMessages,
injectNotificationManager,
OverflowMenu,
ProgressBar,
} from '@modrinth/ui'
import type { Backup } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
const flags = useFeatureFlags()
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const emit = defineEmits<{
(e: 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
@@ -33,11 +42,13 @@ const props = withDefaults(
preview?: boolean
kyrosUrl?: string
jwt?: string
server?: ModrinthServer
}>(),
{
preview: false,
kyrosUrl: undefined,
jwt: undefined,
server: undefined,
},
)
@@ -124,7 +135,48 @@ const messages = defineMessages({
id: 'servers.backups.item.retry',
defaultMessage: 'Retry',
},
downloadingBackup: {
id: 'servers.backups.item.downloading-backup',
defaultMessage: 'Downloading backup...',
},
downloading: {
id: 'servers.backups.item.downloading',
defaultMessage: 'Downloading',
},
})
const downloadingState = ref<{ progress: number; state: string } | undefined>(undefined)
const downloading = computed(() => downloadingState.value)
const handleDownload = async () => {
if (!props.server?.backups || downloading.value) {
return
}
downloadingState.value = { progress: 0, state: 'ongoing' }
try {
const download = props.server.backups.downloadBackup(props.backup.id, props.backup.name)
download.onProgress((p) => {
downloadingState.value = { progress: p.progress, state: 'ongoing' }
})
await download.promise
emit('download')
} catch (error) {
console.error('Failed to download backup:', error)
addNotification({
type: 'error',
title: 'Download failed',
text: error instanceof Error ? error.message : 'Failed to download backup',
})
} finally {
downloadingState.value = undefined
}
}
</script>
<template>
<div
@@ -192,6 +244,15 @@ const messages = defineMessages({
class="max-w-full"
/>
</div>
<div v-else-if="downloading" class="col-span-2 flex flex-col gap-3 text-blue">
{{ formatMessage(messages.downloadingBackup) }}
<ProgressBar
:progress="downloading.progress >= 0 ? downloading.progress : 0"
color="blue"
:waiting="downloading.progress <= 0"
class="max-w-full"
/>
</div>
<template v-else>
<div class="col-span-2">
{{ dayjs(backup.created_at).format('MMMM D, YYYY [at] h:mm A') }}
@@ -223,34 +284,32 @@ const messages = defineMessages({
</button>
</ButtonStyled>
<template v-else>
<ButtonStyled>
<a
:class="{
disabled: !kyrosUrl || !jwt,
}"
:href="`https://${kyrosUrl}/modrinth/v0/backups/${backup.id}/download?auth=${jwt}`"
@click="() => emit('download')"
>
<ButtonStyled v-show="!downloading">
<button :disabled="!server?.backups" @click="handleDownload">
<DownloadIcon />
{{ formatMessage(commonMessages.downloadButton) }}
</a>
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<OverflowMenu
:options="[
{ id: 'rename', action: () => emit('rename') },
{
id: 'rename',
action: () => emit('rename'),
disabled: !!restoring || !!downloading,
},
{
id: 'restore',
action: () => emit('restore'),
disabled: !!restoring,
disabled: !!restoring || !!downloading,
},
{ id: 'lock', action: () => emit('lock') },
{ id: 'lock', action: () => emit('lock'), disabled: !!restoring || !!downloading },
{ divider: true },
{
id: 'delete',
color: 'red',
action: () => emit('delete'),
disabled: !!restoring,
disabled: !!restoring || !!downloading,
},
]"
>

View File

@@ -68,7 +68,11 @@
</ButtonStyled>
<ButtonStyled type="standard" color="brand">
<button :disabled="!canTakeAction" @click="handlePrimaryAction">
<button
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
:disabled="!canTakeAction"
@click="handlePrimaryAction"
>
<div v-if="isTransitionState" class="grid place-content-center">
<LoadingIcon />
</div>
@@ -122,12 +126,15 @@ import { useStorage } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
import LoadingIcon from './icons/LoadingIcon.vue'
import PanelSpinner from './PanelSpinner.vue'
import ServerInfoLabels from './ServerInfoLabels.vue'
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
const flags = useFeatureFlags()
const { formatMessage } = useVIntl()
interface PowerAction {
action: ServerPowerAction
@@ -142,6 +149,7 @@ const props = defineProps<{
serverName?: string
serverData: object
uptimeSeconds: number
backupInProgress?: BackupInProgressReason
}>()
const emit = defineEmits<{
@@ -163,7 +171,11 @@ const dontAskAgain = ref(false)
const startingDelay = ref(false)
const canTakeAction = computed(
() => !props.isActioning && !startingDelay.value && !isTransitionState.value,
() =>
!props.isActioning &&
!startingDelay.value &&
!isTransitionState.value &&
!props.backupInProgress,
)
const isRunning = computed(() => serverState.value === 'running')
const isTransitionState = computed(() =>

View File

@@ -158,11 +158,20 @@ const currentPlanFromSubscription = computed<ServerPlan | undefined>(() => {
: undefined
})
const currentInterval = computed(() => {
const interval = subscription.value?.interval
if (interval === 'monthly' || interval === 'quarterly') {
return interval
}
return 'monthly'
})
async function initiatePayment(body: any): Promise<any> {
if (subscription.value) {
const transformedBody = {
interval: body.charge?.interval,
payment_method: body.id,
payment_method: body.type === 'confirmation_token' ? body.token : body.id,
product: body.charge?.product_id,
region: body.metadata?.server_region,
}
@@ -247,7 +256,7 @@ async function open(id?: string) {
subscription.value = null
}
purchaseModal.value?.show('quarterly')
purchaseModal.value?.show(currentInterval.value)
}
defineExpose({

View File

@@ -11,12 +11,35 @@ export class BackupsModule extends ServerModule {
}
async create(backupName: string): Promise<string> {
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
method: 'POST',
body: { name: backupName },
})
await this.fetch() // Refresh this module
return response.id
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}`
const tempBackup: Backup = {
id: tempId,
name: backupName,
created_at: new Date().toISOString(),
locked: false,
automated: false,
interrupted: false,
ongoing: true,
task: { create: { progress: 0, state: 'ongoing' } },
}
this.data.push(tempBackup)
try {
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
method: 'POST',
body: { name: backupName },
})
const backup = this.data.find((b) => b.id === tempId)
if (backup) {
backup.id = response.id
}
return response.id
} catch (error) {
this.data = this.data.filter((b) => b.id !== tempId)
throw error
}
}
async rename(backupId: string, newName: string): Promise<void> {
@@ -24,35 +47,47 @@ export class BackupsModule extends ServerModule {
method: 'POST',
body: { name: newName },
})
await this.fetch() // Refresh this module
await this.fetch()
}
async delete(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
method: 'DELETE',
})
await this.fetch() // Refresh this module
await this.fetch()
}
async restore(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
method: 'POST',
})
await this.fetch() // Refresh this module
const backup = this.data.find((b) => b.id === backupId)
if (backup) {
if (!backup.task) backup.task = {}
backup.task.restore = { progress: 0, state: 'ongoing' }
}
try {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
method: 'POST',
})
} catch (error) {
if (backup?.task?.restore) {
delete backup.task.restore
}
throw error
}
}
async lock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
method: 'POST',
})
await this.fetch() // Refresh this module
await this.fetch()
}
async unlock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
method: 'POST',
})
await this.fetch() // Refresh this module
await this.fetch()
}
async retry(backupId: string): Promise<void> {
@@ -71,4 +106,87 @@ export class BackupsModule extends ServerModule {
async getAutoBackup(): Promise<AutoBackupSettings> {
return await useServersFetch(`servers/${this.serverId}/autobackup`)
}
downloadBackup(
backupId: string,
backupName: string,
): {
promise: Promise<void>
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void
cancel: () => void
} {
const progressSubject = new EventTarget()
const abortController = new AbortController()
const downloadPromise = new Promise<void>((resolve, reject) => {
const auth = this.server.general?.node
if (!auth?.instance || !auth?.token) {
reject(new Error('Missing authentication credentials'))
return
}
const xhr = new XMLHttpRequest()
xhr.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const progress = e.loaded / e.total
progressSubject.dispatchEvent(
new CustomEvent('progress', {
detail: { loaded: e.loaded, total: e.total, progress },
}),
)
} else {
// progress = -1 to indicate indeterminate size
progressSubject.dispatchEvent(
new CustomEvent('progress', {
detail: { loaded: e.loaded, total: 0, progress: -1 },
}),
)
}
})
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const blob = xhr.response
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${backupName}.zip`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
resolve()
} catch (error) {
reject(error)
}
} else {
reject(new Error(`Download failed with status ${xhr.status}`))
}
}
xhr.onerror = () => reject(new Error('Download failed'))
xhr.onabort = () => reject(new Error('Download cancelled'))
xhr.open(
'GET',
`https://${auth.instance}/modrinth/v0/backups/${backupId}/download?auth=${auth.token}`,
)
xhr.responseType = 'blob'
xhr.send()
abortController.signal.addEventListener('abort', () => xhr.abort())
})
return {
promise: downloadPromise,
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => {
progressSubject.addEventListener('progress', ((e: CustomEvent) => {
cb(e.detail)
}) as EventListener)
},
cancel: () => abortController.abort(),
}
}
}

View File

@@ -6,7 +6,7 @@ export interface ServersFetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
contentType?: string
body?: Record<string, any>
version?: number
version?: number | 'internal'
override?: {
url?: string
token?: string
@@ -30,7 +30,7 @@ export async function useServersFetch<T>(
'[Modrinth Servers] Cannot fetch without auth',
10000,
)
throw new ModrinthServerError('Missing auth token', 401, error, module)
throw new ModrinthServerError('Missing auth token', 401, error, module, undefined, undefined)
}
const {
@@ -52,7 +52,14 @@ export async function useServersFetch<T>(
'[Modrinth Servers] Circuit breaker open - too many recent failures',
503,
)
throw new ModrinthServerError('Service temporarily unavailable', 503, error, module)
throw new ModrinthServerError(
'Service temporarily unavailable',
503,
error,
module,
undefined,
undefined,
)
}
if (now - lastFailureTime.value > 30000) {
@@ -69,7 +76,14 @@ export async function useServersFetch<T>(
'[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables',
10001,
)
throw new ModrinthServerError('Configuration error: Missing PYRO_BASE_URL', 500, error, module)
throw new ModrinthServerError(
'Configuration error: Missing PYRO_BASE_URL',
500,
error,
module,
undefined,
undefined,
)
}
const versionString = `v${version}`
@@ -82,7 +96,9 @@ export async function useServersFetch<T>(
? `https://${newOverrideUrl}/${path.replace(/^\//, '')}`
: version === 0
? `${base}/modrinth/v${version}/${path.replace(/^\//, '')}`
: `${base}/v${version}/${path.replace(/^\//, '')}`
: version === 'internal'
? `${base}/_internal/${path.replace(/^\//, '')}`
: `${base}/v${version}/${path.replace(/^\//, '')}`
const headers: Record<string, string> = {
'User-Agent': 'Modrinth/1.0 (https://modrinth.com)',
@@ -177,6 +193,7 @@ export async function useServersFetch<T>(
fetchError,
module,
v1Error,
error.data,
)
}
@@ -198,6 +215,8 @@ export async function useServersFetch<T>(
undefined,
fetchError,
module,
undefined,
undefined,
)
}
}
@@ -210,7 +229,14 @@ export async function useServersFetch<T>(
statusCode,
lastError,
)
throw new ModrinthServerError('Maximum retry attempts reached', statusCode, pyroError, module)
throw new ModrinthServerError(
'Maximum retry attempts reached',
statusCode,
pyroError,
module,
undefined,
lastError.data,
)
}
const fetchError = new ModrinthServersFetchError(
@@ -218,5 +244,12 @@ export async function useServersFetch<T>(
undefined,
lastError || undefined,
)
throw new ModrinthServerError('Maximum retry attempts reached', undefined, fetchError, module)
throw new ModrinthServerError(
'Maximum retry attempts reached',
undefined,
fetchError,
module,
undefined,
undefined,
)
}

View File

@@ -82,6 +82,23 @@
</ButtonStyled>
</template>
</PagewideBanner>
<PagewideBanner v-if="showTinMismatchBanner" variant="error">
<template #title>
<span>{{ formatMessage(tinMismatchBannerMessages.title) }}</span>
</template>
<template #description>
<span>{{ formatMessage(tinMismatchBannerMessages.description) }}</span>
</template>
<template #actions>
<div class="flex w-fit flex-row">
<ButtonStyled color="red">
<nuxt-link to="https://support.modrinth.com" target="_blank" rel="noopener">
<MessageIcon /> {{ formatMessage(tinMismatchBannerMessages.action) }}
</nuxt-link>
</ButtonStyled>
</div>
</template>
</PagewideBanner>
<PagewideBanner v-if="showTaxComplianceBanner" variant="warning">
<template #title>
<span>{{ formatMessage(taxBannerMessages.title) }}</span>
@@ -444,6 +461,12 @@
link: '/admin/servers/notices',
shown: isAdmin(auth.user),
},
{
id: 'servers-nodes',
color: 'primary',
link: '/admin/servers/nodes',
shown: isAdmin(auth.user),
},
]"
>
<ModrinthIcon aria-hidden="true" />
@@ -463,6 +486,7 @@
<template #servers-notices>
<IssuesIcon aria-hidden="true" /> {{ formatMessage(messages.manageServerNotices) }}
</template>
<template #servers-nodes> <ServerIcon aria-hidden="true" /> Server Nodes </template>
</OverflowMenu>
</ButtonStyled>
<ButtonStyled type="transparent">
@@ -851,6 +875,7 @@ import {
LogInIcon,
LogOutIcon,
MastodonIcon,
MessageIcon,
ModrinthIcon,
MoonIcon,
OrganizationIcon,
@@ -918,7 +943,15 @@ const showTaxComplianceBanner = computed(() => {
const thresholdMet = (bal.withdrawn_ytd ?? 0) >= 600
const status = bal.form_completion_status ?? 'unknown'
const isComplete = status === 'complete'
return !!auth.value.user && thresholdMet && !isComplete
const isTinMismatch = status === 'tin-mismatch'
return !!auth.value.user && thresholdMet && !isComplete && !isTinMismatch
})
const showTinMismatchBanner = computed(() => {
const bal = payoutBalance.value
if (!bal) return false
const status = bal.form_completion_status ?? 'unknown'
return !!auth.value.user && status === 'tin-mismatch'
})
const taxBannerMessages = defineMessages({
@@ -929,7 +962,7 @@ const taxBannerMessages = defineMessages({
description: {
id: 'layout.banner.tax.description',
defaultMessage:
'Youve already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted.',
"You've already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted.",
},
action: {
id: 'layout.banner.tax.action',
@@ -937,6 +970,22 @@ const taxBannerMessages = defineMessages({
},
})
const tinMismatchBannerMessages = defineMessages({
title: {
id: 'layout.banner.tin-mismatch.title',
defaultMessage: 'Tax form failed',
},
description: {
id: 'layout.banner.tin-mismatch.description',
defaultMessage:
"Your withdrawals are temporarily locked because your TIN or SSN didn't match IRS records. Please contact support to reset and resubmit your tax form.",
},
action: {
id: 'layout.banner.tin-mismatch.action',
defaultMessage: 'Contact support',
},
})
const taxFormModalRef = ref(null)
function openTaxForm(e) {
if (taxFormModalRef.value && taxFormModalRef.value.startTaxForm) {
@@ -1732,6 +1781,7 @@ const footerLinks = [
@media screen and (min-width: 354px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (min-width: 674px) {
grid-template-columns: repeat(3, 1fr);
}
@@ -1926,10 +1976,12 @@ const footerLinks = [
width: 25rem;
height: 25rem;
}
.animation-ring-2 {
width: 50rem;
height: 50rem;
}
.animation-ring-3 {
width: 100rem;
height: 100rem;
@@ -1958,15 +2010,19 @@ const footerLinks = [
0% {
rotate: 0deg;
}
25% {
rotate: calc(1deg * (var(--_r-count) - 20));
}
50% {
rotate: 0deg;
}
75% {
rotate: calc(-1deg * (var(--_r-count) - 20));
}
100% {
rotate: 0deg;
}
@@ -1976,15 +2032,19 @@ const footerLinks = [
0% {
translate: 0;
}
25% {
translate: calc(2px * (var(--_r-count) - 20));
}
50% {
translate: 0;
}
75% {
translate: calc(-2px * (var(--_r-count) - 20));
}
100% {
translate: 0;
}
@@ -1994,15 +2054,19 @@ const footerLinks = [
0% {
transform: translateY(0);
}
25% {
transform: translateY(calc(2px * (var(--_r-count) - 20)));
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(calc(-2px * (var(--_r-count) - 20)));
}
100% {
transform: translateY(0);
}

View File

@@ -924,11 +924,20 @@
"message": "Complete tax form"
},
"layout.banner.tax.description": {
"message": "Youve already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted."
"message": "You've already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted."
},
"layout.banner.tax.title": {
"message": "Tax form required"
},
"layout.banner.tin-mismatch.action": {
"message": "Contact support"
},
"layout.banner.tin-mismatch.description": {
"message": "Your withdrawals are temporarily locked because your TIN or SSN didn't match IRS records. Please contact support to reset and resubmit your tax form."
},
"layout.banner.tin-mismatch.title": {
"message": "Tax form failed"
},
"layout.banner.verify-email.action": {
"message": "Re-send verification email"
},
@@ -1103,6 +1112,12 @@
"moderation.technical.search.placeholder": {
"message": "Search tech reviews..."
},
"profile.bio.fallback.creator": {
"message": "A Modrinth creator."
},
"profile.bio.fallback.user": {
"message": "A Modrinth user."
},
"profile.button.billing": {
"message": "Manage user billing"
},
@@ -1115,18 +1130,48 @@
"profile.button.manage-projects": {
"message": "Manage projects"
},
"profile.details.label.auth-providers": {
"message": "Auth providers"
},
"profile.details.label.email": {
"message": "Email"
},
"profile.details.label.has-password": {
"message": "Has password"
},
"profile.details.label.has-totp": {
"message": "Has TOTP"
},
"profile.details.label.payment-methods": {
"message": "Payment methods"
},
"profile.details.tooltip.email-not-verified": {
"message": "Email not verified"
},
"profile.details.tooltip.email-verified": {
"message": "Email verified"
},
"profile.error.not-found": {
"message": "User not found"
},
"profile.joined-at": {
"message": "Joined <date>{ago}</date>"
},
"profile.label.badges": {
"message": "Badges"
},
"profile.label.collection": {
"message": "Collection"
},
"profile.label.details": {
"message": "Details"
},
"profile.label.downloads": {
"message": "{count} {count, plural, one {download} other {downloads}}"
},
"profile.label.joined": {
"message": "Joined"
},
"profile.label.no": {
"message": "No"
},
"profile.label.no-collections": {
"message": "This user has no collections!"
},
@@ -1142,18 +1187,21 @@
"profile.label.organizations": {
"message": "Organizations"
},
"profile.label.projects": {
"message": "{count} {count, plural, one {project} other {projects}}"
},
"profile.label.saving": {
"message": "Saving..."
},
"profile.label.yes": {
"message": "Yes"
},
"profile.meta.description": {
"message": "Download {username}'s projects on Modrinth"
},
"profile.meta.description-with-bio": {
"message": "{bio} - Download {username}'s projects on Modrinth"
},
"profile.stats.downloads": {
"message": "{count, plural, one {<stat>{count}</stat> project download} other {<stat>{count}</stat> project downloads}}"
},
"profile.stats.projects": {
"message": "{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}"
},
"profile.stats.projects-followers": {
"message": "{count, plural, one {<stat>{count}</stat> project follower} other {<stat>{count}</stat> project followers}}"
},
@@ -1886,6 +1934,12 @@
"servers.backups.item.creating-backup": {
"message": "Creating backup..."
},
"servers.backups.item.downloading": {
"message": "Downloading"
},
"servers.backups.item.downloading-backup": {
"message": "Downloading backup..."
},
"servers.backups.item.failed-to-create-backup": {
"message": "Failed to create backup"
},

View File

@@ -97,6 +97,41 @@
</div>
</div>
</NewModal>
<NewModal ref="creditModal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Credit subscription</span>
</template>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="days" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">Days to credit</span>
<span>Enter the number of days to add to the next due date.</span>
</label>
<input id="days" v-model.number="creditDays" type="number" min="1" autocomplete="off" />
</div>
<div class="flex flex-col gap-2">
<label for="sendEmail" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast">Send email to user</span>
<span>Notify the user about the credited days.</span>
</label>
<Toggle id="sendEmail" v-model="creditSendEmail" />
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="crediting" @click="applyCredit">
<CheckIcon aria-hidden="true" />
Apply credit
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="creditModal.hide()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<div class="page experimental-styles-within">
<div
class="mb-4 flex items-center justify-between border-0 border-b border-solid border-divider pb-4"
@@ -140,6 +175,7 @@
</div>
</div>
<div v-if="subscription.metadata?.id" class="flex flex-col items-end gap-2">
<CopyCode :text="subscription.metadata.id" />
<ButtonStyled
v-if="
subscription.metadata?.type === 'pyro' || subscription.metadata?.type === 'medal'
@@ -153,7 +189,12 @@
<ServerIcon /> Server panel <ExternalIcon class="h-4 w-4" />
</nuxt-link>
</ButtonStyled>
<CopyCode :text="subscription.metadata.id" />
<ButtonStyled>
<button @click="showCreditModal(subscription)">
<CurrencyIcon />
Credit
</button>
</ButtonStyled>
</div>
</div>
<div class="flex flex-col gap-2">
@@ -292,6 +333,7 @@ import {
useRelativeTime,
} from '@modrinth/ui'
import { formatCategory, formatPrice } from '@modrinth/utils'
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
import dayjs from 'dayjs'
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
@@ -370,6 +412,11 @@ const modifying = ref(false)
const modifyModal = ref()
const cancel = ref(false)
const crediting = ref(false)
const creditModal = ref()
const creditDays = ref(7)
const creditSendEmail = ref(true)
function showRefundModal(charge) {
selectedCharge.value = charge
refundType.value = 'full'
@@ -385,6 +432,44 @@ function showModifyModal(charge, subscription) {
modifyModal.value.show()
}
function showCreditModal(subscription) {
selectedSubscription.value = subscription
creditDays.value = 1
creditSendEmail.value = true
creditModal.value.show()
}
async function applyCredit() {
crediting.value = true
try {
const daysParsed = Math.max(1, Math.floor(Number(creditDays.value) || 1))
await useBaseFetch('billing/credit', {
method: 'POST',
body: JSON.stringify({
subscription_ids: [selectedSubscription.value.id],
days: daysParsed,
send_email: creditSendEmail.value,
message: DEFAULT_CREDIT_EMAIL_MESSAGE,
}),
internal: true,
})
addNotification({
title: 'Credit applied',
text: 'The subscription due date has been updated.',
type: 'success',
})
await refreshCharges()
creditModal.value.hide()
} catch (err) {
addNotification({
title: 'Error applying credit',
text: err.data?.description ?? String(err),
type: 'error',
})
}
crediting.value = false
}
async function refundCharge() {
refunding.value = true
try {

View File

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

View File

@@ -18,12 +18,17 @@
<section class="auth-form">
<p>{{ formatMessage(postVerificationMessages.description) }}</p>
<NuxtLink v-if="auth.user" class="btn" link="/settings/account">
<SettingsIcon /> {{ formatMessage(messages.accountSettings) }}
</NuxtLink>
<NuxtLink v-else to="/auth/sign-in" class="btn btn-primary continue-btn centered-btn">
{{ formatMessage(messages.signIn) }} <RightArrowIcon />
</NuxtLink>
<ButtonStyled v-if="auth.user">
<NuxtLink to="/settings/account">
<SettingsIcon /> {{ formatMessage(messages.accountSettings) }}
</NuxtLink>
</ButtonStyled>
<ButtonStyled v-else>
<NuxtLink to="/auth/sign-in">
{{ formatMessage(messages.signIn) }}
<RightArrowIcon />
</NuxtLink>
</ButtonStyled>
</section>
</template>
@@ -40,24 +45,26 @@
</template>
</p>
<button
v-if="auth.user"
class="btn btn-primary continue-btn"
@click="handleResendEmailVerification"
>
{{ formatMessage(failedVerificationMessages.action) }} <RightArrowIcon />
</button>
<ButtonStyled v-if="auth.user" color="brand">
<button @click="handleResendEmailVerification">
{{ formatMessage(failedVerificationMessages.action) }}
<RightArrowIcon />
</button>
</ButtonStyled>
<NuxtLink v-else to="/auth/sign-in" class="btn btn-primary continue-btn centered-btn">
{{ formatMessage(messages.signIn) }} <RightArrowIcon />
</NuxtLink>
<ButtonStyled v-else color="brand">
<NuxtLink to="/auth/sign-in">
{{ formatMessage(messages.signIn) }}
<RightArrowIcon />
</NuxtLink>
</ButtonStyled>
</section>
</template>
</div>
</template>
<script setup>
import { RightArrowIcon, SettingsIcon } from '@modrinth/assets'
import { injectNotificationManager } from '@modrinth/ui'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()

View File

@@ -146,7 +146,7 @@
before proceeding.
</p>
<p v-if="blockedByTax" class="font-bold text-orange">
<p v-else-if="blockedByTax" class="font-bold text-orange">
You have withdrawn over $600 this year. To continue withdrawing, you must complete a tax form.
</p>

View File

@@ -4,7 +4,7 @@ import { articles as rawArticles } from '@modrinth/blog'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import type { User } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import NewsletterButton from '~/components/ui/NewsletterButton.vue'
import ShareArticleButtons from '~/components/ui/ShareArticleButtons.vue'
@@ -74,6 +74,36 @@ useSeoMeta({
twitterCard: 'summary_large_image',
twitterImage: () => thumbnailPath.value,
})
onMounted(() => {
const videos = document.querySelectorAll('.markdown-body video')
if ('IntersectionObserver' in window) {
const videoObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const video = entry.target as HTMLVideoElement
if (entry.isIntersecting) {
video.play().catch(() => {})
} else {
video.pause()
}
})
},
{
threshold: 0.5,
},
)
videos.forEach((video) => {
videoObserver.observe(video)
})
} else {
videos.forEach((video) => {
;(video as HTMLVideoElement).setAttribute('autoplay', '')
})
}
})
</script>
<template>
@@ -181,14 +211,19 @@ useSeoMeta({
padding: 0;
}
ul,
ul > li:not(:last-child),
ol > li:not(:last-child) {
margin-bottom: 0.5rem;
}
ul,
ol {
p {
> li > p {
margin-top: 0;
margin-bottom: 0;
}
> li > p:not(:last-child) {
margin-bottom: 0.5rem;
}
}
@@ -220,20 +255,22 @@ useSeoMeta({
h2 {
font-size: 1.25rem;
margin-top: 1.5rem;
@media (min-width: 640px) {
font-size: 1.5rem;
}
}
h3 {
font-size: 1.125rem;
font-size: 1rem;
margin-top: 1.25rem;
@media (min-width: 640px) {
font-size: 1.25rem;
font-size: 1.125rem;
}
}
p {
margin-bottom: 1.25rem;
margin-bottom: 0.75rem;
font-size: 0.875rem;
@media (min-width: 640px) {
font-size: 1rem;
@@ -275,8 +312,34 @@ useSeoMeta({
}
}
hr {
border: none;
height: 1px;
background-color: var(--color-divider);
}
.video-wrapper {
display: inline-block;
max-width: 100%;
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-button-border);
@media (min-width: 640px) {
border-radius: var(--radius-lg);
}
video {
display: block;
max-width: 100%;
object-fit: cover;
height: auto;
}
}
> img,
> :has(img:first-child:last-child) {
> .video-wrapper,
> :has(img:first-child:last-child),
> :has(video:first-child:last-child) {
display: flex;
justify-content: center;
}

View File

@@ -151,6 +151,7 @@
:server-name="serverData.name"
:server-data="serverData"
:uptime-seconds="uptimeSeconds"
:backup-in-progress="backupInProgress"
@action="sendPowerAction"
/>
</div>
@@ -354,7 +355,7 @@
>
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
JSON.stringify(server, null, ' ')
JSON.stringify(server, null, ' ')
}}</pre>
</div>
</template>
@@ -759,9 +760,14 @@ const handleWebSocketMessage = (data: WSEvent) => {
curBackup.task = {}
}
curBackup.task[data.task] = {
progress: data.progress,
state: data.state,
const currentState = curBackup.task[data.task]?.state
const shouldUpdate = !(currentState === 'ongoing' && data.state === 'unchanged')
if (shouldUpdate) {
curBackup.task[data.task] = {
progress: data.progress,
state: data.state,
}
}
curBackup.ongoing = data.task === 'create' && data.state === 'ongoing'
@@ -1037,7 +1043,10 @@ const nodeUnavailableDetails = computed(() => [
},
{
label: 'Node',
value: server.general?.datacenter ?? 'Unknown',
value:
server.moduleErrors?.general?.error.responseData?.hostname ??
server.general?.datacenter ??
'Unknown',
type: 'inline' as const,
},
{
@@ -1277,6 +1286,7 @@ useHead({
opacity: 0;
transform: translateX(1rem);
}
100% {
opacity: 1;
transform: none;

View File

@@ -105,6 +105,7 @@
v-for="backup in backups"
:key="`backup-${backup.id}`"
:backup="backup"
:server="props.server"
:kyros-url="props.server.general?.node.instance"
:jwt="props.server.general?.node.token"
@download="() => triggerDownloadAnimation()"

View File

@@ -16,7 +16,7 @@
<ButtonStyled>
<button @click="cancelRoleEdit">
<XIcon />
Cancel
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
@@ -25,9 +25,11 @@
@click="saveRoleEdit"
>
<template v-if="isSavingRole">
<SpinnerIcon class="animate-spin" /> Saving...
<SpinnerIcon class="animate-spin" /> {{ formatMessage(messages.savingLabel) }}
</template>
<template v-else>
<SaveIcon /> {{ formatMessage(commonMessages.saveChangesButton) }}
</template>
<template v-else> <SaveIcon /> Save changes </template>
</button>
</ButtonStyled>
</div>
@@ -36,10 +38,16 @@
<NewModal v-if="auth.user && isStaff(auth.user)" ref="userDetailsModal" header="User details">
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1">
<span class="text-lg font-bold text-primary">Email</span>
<span class="text-lg font-bold text-primary">{{
formatMessage(messages.emailLabel)
}}</span>
<div>
<span
v-tooltip="user.email_verified ? 'Email verified' : 'Email not verified'"
v-tooltip="
user.email_verified
? formatMessage(messages.emailVerifiedTooltip)
: formatMessage(messages.emailNotVerifiedTooltip)
"
class="flex w-fit items-center gap-1"
>
<span>{{ user.email }}</span>
@@ -50,12 +58,16 @@
</div>
<div class="flex flex-col gap-1">
<span class="text-lg font-bold text-primary"> Auth providers </span>
<span class="text-lg font-bold text-primary">{{
formatMessage(messages.authProvidersLabel)
}}</span>
<span>{{ user.auth_providers.join(', ') }}</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-lg font-bold text-primary"> Payment methods</span>
<span class="text-lg font-bold text-primary">{{
formatMessage(messages.paymentMethodsLabel)
}}</span>
<span>
<template v-if="user.payout_data?.paypal_address">
Paypal ({{ user.payout_data.paypal_address }} - {{ user.payout_data.paypal_country }})
@@ -70,16 +82,22 @@
</div>
<div class="flex flex-col gap-1">
<span class="text-lg font-bold text-primary"> Has password </span>
<span class="text-lg font-bold text-primary">{{
formatMessage(messages.hasPasswordLabel)
}}</span>
<span>
{{ user.has_password ? 'Yes' : 'No' }}
{{
user.has_password ? formatMessage(messages.yesLabel) : formatMessage(messages.noLabel)
}}
</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-lg font-bold text-primary"> Has TOTP </span>
<span class="text-lg font-bold text-primary">{{
formatMessage(messages.hasTotpLabel)
}}</span>
<span>
{{ user.has_totp ? 'Yes' : 'No' }}
{{ user.has_totp ? formatMessage(messages.yesLabel) : formatMessage(messages.noLabel) }}
</span>
</div>
</div>
@@ -98,8 +116,8 @@
user.bio
? user.bio
: projects.length === 0
? 'A Modrinth user.'
: 'A Modrinth creator.'
? formatMessage(messages.bioFallbackUser)
: formatMessage(messages.bioFallbackCreator)
}}
</template>
<template #stats>
@@ -107,16 +125,22 @@
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
{{
formatMessage(messages.profileProjectsLabel, {
count: formatCompactNumber(projects?.length || 0),
})
}}
</div>
<div
v-tooltip="sumDownloads.toLocaleString()"
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(sumDownloads) }}
downloads
{{
formatMessage(messages.profileDownloadsLabel, {
count: formatCompactNumber(sumDownloads),
})
}}
</div>
<div
v-tooltip="
@@ -128,7 +152,7 @@
class="flex items-center gap-2 font-semibold"
>
<CalendarIcon class="h-6 w-6 text-secondary" />
Joined
{{ formatMessage(messages.profileJoinedLabel) }}
{{ formatRelativeTime(user.created) }}
</div>
</template>
@@ -287,7 +311,7 @@
<h2 class="title">{{ collection.name }}</h2>
<div class="stats">
<LibraryIcon aria-hidden="true" />
Collection
{{ formatMessage(messages.collectionLabel) }}
</div>
</div>
</div>
@@ -298,25 +322,27 @@
<div class="stats">
<BoxIcon />
{{
`${$formatNumber(collection.projects?.length || 0, false)} project${(collection.projects?.length || 0) !== 1 ? 's' : ''}`
`${$formatNumber(collection.projects?.length || 0, false)} project${
(collection.projects?.length || 0) !== 1 ? 's' : ''
}`
}}
</div>
<div class="stats">
<template v-if="collection.status === 'listed'">
<GlobeIcon />
<span> Public </span>
<span> {{ formatMessage(commonMessages.publicLabel) }} </span>
</template>
<template v-else-if="collection.status === 'unlisted'">
<LinkIcon />
<span> Unlisted </span>
<span> {{ formatMessage(commonMessages.unlistedLabel) }} </span>
</template>
<template v-else-if="collection.status === 'private'">
<LockIcon />
<span> Private </span>
<span> {{ formatMessage(commonMessages.privateLabel) }} </span>
</template>
<template v-else-if="collection.status === 'rejected'">
<XIcon />
<span> Rejected </span>
<span> {{ formatMessage(commonMessages.rejectedLabel) }} </span>
</template>
</div>
</div>
@@ -449,25 +475,75 @@ const formatRelativeTime = useRelativeTime()
const { addNotification } = injectNotificationManager()
const messages = defineMessages({
profileProjectsStats: {
id: 'profile.stats.projects',
defaultMessage:
'{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}',
profileProjectsLabel: {
id: 'profile.label.projects',
defaultMessage: '{count} {count, plural, one {project} other {projects}}',
},
profileDownloadsStats: {
id: 'profile.stats.downloads',
defaultMessage:
'{count, plural, one {<stat>{count}</stat> project download} other {<stat>{count}</stat> project downloads}}',
profileDownloadsLabel: {
id: 'profile.label.downloads',
defaultMessage: '{count} {count, plural, one {download} other {downloads}}',
},
profileJoinedLabel: {
id: 'profile.label.joined',
defaultMessage: 'Joined',
},
savingLabel: {
id: 'profile.label.saving',
defaultMessage: 'Saving...',
},
emailLabel: {
id: 'profile.details.label.email',
defaultMessage: 'Email',
},
emailVerifiedTooltip: {
id: 'profile.details.tooltip.email-verified',
defaultMessage: 'Email verified',
},
emailNotVerifiedTooltip: {
id: 'profile.details.tooltip.email-not-verified',
defaultMessage: 'Email not verified',
},
authProvidersLabel: {
id: 'profile.details.label.auth-providers',
defaultMessage: 'Auth providers',
},
paymentMethodsLabel: {
id: 'profile.details.label.payment-methods',
defaultMessage: 'Payment methods',
},
hasPasswordLabel: {
id: 'profile.details.label.has-password',
defaultMessage: 'Has password',
},
hasTotpLabel: {
id: 'profile.details.label.has-totp',
defaultMessage: 'Has TOTP',
},
yesLabel: {
id: 'profile.label.yes',
defaultMessage: 'Yes',
},
noLabel: {
id: 'profile.label.no',
defaultMessage: 'No',
},
bioFallbackUser: {
id: 'profile.bio.fallback.user',
defaultMessage: 'A Modrinth user.',
},
bioFallbackCreator: {
id: 'profile.bio.fallback.creator',
defaultMessage: 'A Modrinth creator.',
},
collectionLabel: {
id: 'profile.label.collection',
defaultMessage: 'Collection',
},
profileProjectsFollowersStats: {
id: 'profile.stats.projects-followers',
defaultMessage:
'{count, plural, one {<stat>{count}</stat> project follower} other {<stat>{count}</stat> project followers}}',
},
profileJoinedAt: {
id: 'profile.joined-at',
defaultMessage: 'Joined <date>{ago}</date>',
},
profileUserId: {
id: 'profile.user-id',
defaultMessage: 'User ID: {id}',

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,5 +1,12 @@
{
"articles": [
{
"title": "More Ways to Withdraw",
"summary": "Coming soon: new withdraw options and a redesigned revenue dashboard",
"thumbnail": "https://modrinth.com/news/article/creator-withdrawals-overhaul/thumbnail.webp",
"date": "2025-10-27T23:30:00.000Z",
"link": "https://modrinth.com/news/article/creator-withdrawals-overhaul"
},
{
"title": "Standing By Our Values",
"summary": "Keeping LGBTQIA+ content visible despite demands from Russia.",

File diff suppressed because one or more lines are too long

View File

@@ -33,7 +33,6 @@
}
#modrinth-rail-1 {
border-radius: 1rem;
position: absolute;
left: 0;
top: 0;

View File

@@ -90,10 +90,10 @@ import StyledDoc from '../shared/StyledDoc.vue'
Purpose of Payment
</Text>
<Text class="m-0 text-sm leading-relaxed text-secondary">
This payout reflects revenue earned by the creator through their activity on the Modrinth
platform. Earnings are based on advertising revenue, subscriptions, and/or affiliate
commissions tied to the creator's published projects, in accordance with the Rewards Program
Terms.
This payout reflects the creator's earnings from their activity on the Modrinth platform.
Such earnings are based on advertising revenue derived from user engagement with the
creator's published projects and/or affiliate commissions in accordance with the Rewards
Program Terms.
</Text>
</Section>

View File

@@ -17,9 +17,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
Modrinth account.
</Text>
<Text class="text-muted text-base">
If you did not make this change, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not make this change, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -17,9 +17,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
sign in to your Modrinth account.
</Text>
<Text class="text-muted text-base">
If you did not make this change, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not make this change, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -17,9 +17,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
{emailchanged.new_email}.
</Text>
<Text class="text-muted text-base">
If you did not make this change, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not make this change, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -70,9 +70,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
<Text class="text-muted text-base">
If this wasn't you, please update your password and review your account security settings. If
you cannot do this, contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
you cannot do this, contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -18,9 +18,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
A new personal access token, <b>{newpat.token_name}</b>, has been added to your account.
</Text>
<Text class="text-muted text-base">
If you did not create this token, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not create this token, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -14,9 +14,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
<Text class="text-muted text-base">Hi {user.name},</Text>
<Text class="text-muted text-base"> Your password has been changed on your account. </Text>
<Text class="text-muted text-base">
If you did not make this change, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not make this change, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -18,9 +18,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
Modrinth account.</Text
>
<Text class="text-muted text-base">
If you did not make this change, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not make this change, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -34,8 +34,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
<Text class="text-base">
If you have any questions about the creator rewards program, please contact support through
the
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink>
or by replying to this email.
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
<Text class="text-base">Thank you for being a creator on Modrinth!</Text>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { Heading, Text } from '@vue-email/components'
import StyledEmail from '../shared/StyledEmail.vue'
</script>
<template>
<StyledEmail title="Weve added time to your server">
<Heading as="h1" class="mb-2 text-2xl font-bold">Weve added time to your server</Heading>
<Text class="text-muted text-base">Hi {user.name},</Text>
<Text class="text-muted text-base">{credit.header_message}</Text>
<Text class="text-muted text-base">
To make up for it, we've added {credit.days_formatted} to your {credit.subscription.type}
subscription.
</Text>
<Text class="text-muted text-base">
Your next charge was scheduled for {credit.previous_due} and will now be on {credit.next_due}.
</Text>
<Text class="text-muted text-base">Thank you for supporting us,</Text>
<Text class="text-muted text-base">The Modrinth Team</Text>
</StyledEmail>
</template>

View File

@@ -32,7 +32,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
<Text class="text-muted text-base">
Thank you for choosing Modrinth! If you have any questions or need help with your
subscription, reply to this email or visit our
subscription, visit our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>

View File

@@ -17,9 +17,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
need to submit the code generated by your authenticator app.
</Text>
<Text class="text-muted text-base">
If you did not make this change, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not make this change, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -18,9 +18,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
At your request, we've removed two-factor authentication from your Modrinth account.
</Text>
<Text class="text-muted text-base">
If you did not make this change, please contact us immediately by replying to this email or
<VLink href="https://support.modrinth.com" class="text-green underline"
>through our Support Portal</VLink
If you did not make this change, please contact us immediately through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>

View File

@@ -18,6 +18,7 @@ export default {
// Subscriptions
'subscription-tax-change': () => import('./account/SubscriptionTaxChange.vue'),
'subscription-credited': () => import('./account/SubscriptionCredited.vue'),
// Moderation
'report-submitted': () => import('./moderation/ReportSubmitted.vue'),

View File

@@ -68,8 +68,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
in error or is abusive, please contact support
<VLink href="https://support.modrinth.com" class="text-green underline">
through the Support Portal</VLink
>
or by replying to this email.
>.
</Text>
</StyledEmail>
</template>

View File

@@ -63,8 +63,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
this was sent in error or is abusive, please contact support
<VLink href="https://support.modrinth.com" class="text-green underline">
through the Support Portal</VLink
>
or by replying to this email.
>.
</Text>
</StyledEmail>
</template>

View File

@@ -45,8 +45,7 @@ import StyledEmail from '../shared/StyledEmail.vue'
</Button>
<Text class="text-base">
If you have questions or believe something isn't correct, you can reply to this email or reach
out via the
If you have questions or believe something isn't correct, you can reach out via the
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>

View File

@@ -53,8 +53,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
<Text class="text-base">
If you believe this status was applied in error, you can reply in the moderation thread or
contact support through our
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink>
or by replying to this email.
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
<Text class="text-base">Thank you for publishing on Modrinth!</Text>

View File

@@ -53,8 +53,8 @@ import StyledEmail from '../shared/StyledEmail.vue'
<Text class="text-base">
If you did not initiate this transfer, please contact support immediately through the
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink>
or by replying to this email.
<VLink href="https://support.modrinth.com" class="text-green underline">Support Portal</VLink
>.
</Text>
</StyledEmail>
</template>

View File

@@ -307,6 +307,7 @@ export const useFetchAllAnalytics = (
startDate = ref(dayjs().subtract(30, 'days')),
endDate = ref(dayjs()),
timeResolution = ref(1440),
isInitialized = ref(false),
) => {
const downloadData = ref(null)
const viewData = ref(null)
@@ -388,8 +389,18 @@ export const useFetchAllAnalytics = (
}
watch(
[() => startDate.value, () => endDate.value, () => timeResolution.value, () => projects.value],
[
() => startDate.value,
() => endDate.value,
() => timeResolution.value,
() => projects.value,
() => isInitialized.value,
],
async () => {
if (!isInitialized.value) {
return
}
const q = {
start_date: startDate.value.toISOString(),
end_date: endDate.value.toISOString(),
@@ -456,5 +467,6 @@ export const useFetchAllAnalytics = (
totalData,
loading,
error,
isInitialized,
}
}

View File

@@ -114,6 +114,7 @@ CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=default
CLICKHOUSE_DATABASE=staging_ariadne
MAXMIND_ACCOUNT_ID=none
MAXMIND_LICENSE_KEY=none
FLAME_ANVIL_URL=none
@@ -141,4 +142,7 @@ COMPLIANCE_PAYOUT_THRESHOLD=disabled
ANROK_API_KEY=none
ANROK_API_URL=none
GOTENBERG_URL=http://labrinth-gotenberg:13000
GOTENBERG_CALLBACK_BASE=http://host.docker.internal:8000/_internal/gotenberg
ARCHON_URL=none

View File

@@ -115,6 +115,7 @@ CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=default
CLICKHOUSE_DATABASE=staging_ariadne
MAXMIND_ACCOUNT_ID=none
MAXMIND_LICENSE_KEY=none
FLAME_ANVIL_URL=none
@@ -142,4 +143,7 @@ COMPLIANCE_PAYOUT_THRESHOLD=disabled
ANROK_API_KEY=none
ANROK_API_URL=none
GOTENBERG_URL=http://localhost:13000
GOTENBERG_CALLBACK_BASE=http://host.docker.internal:8000/_internal/gotenberg
ARCHON_URL=none

View File

@@ -113,6 +113,10 @@ migrations/20250725230041_reports-closed-status-index.sql
migrations/20250727184120_user-newsletter-subscription-column.sql
migrations/20250804221014_users-redeemals.sql
migrations/20250805001654_product-prices-public.sql
migrations/20250823233518_user-compliance.sql
migrations/20250902133943_notification-extension.sql
migrations/20250914190749_affiliate_codes.sql
migrations/20250927120742_user_limits.sql
# Prettier reformats some of the PostgreSQL-specific COPY syntax here,
# which is very likely to break things

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
},
{
"ordinal": 19,
"name": "tax_transaction_version?",
"type_info": "Int4"
},
{
"ordinal": 20,
"name": "tax_platform_accounting_time?",
"type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54"
"hash": "372c03a6daf0045f615faa9a6205558cd0ea1d9dba5948e8fa2496ed99de8fea"
}

View File

@@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users_subscriptions_credits\n (subscription_id, user_id, creditor_id, days, previous_due, next_due)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bigint[], $4::int[], $5::timestamptz[], $6::timestamptz[])\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8Array",
"Int8Array",
"Int8Array",
"Int4Array",
"TimestamptzArray",
"TimestamptzArray"
]
},
"nullable": []
},
"hash": "3d05de766a0987028d465a3305938164e4d79734c17c07111f8923f2faa517bf"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE parent_charge_id = $1",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
},
{
"ordinal": 19,
"name": "tax_transaction_version?",
"type_info": "Int4"
},
{
"ordinal": 20,
"name": "tax_platform_accounting_time?",
"type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda"
"hash": "4a0e5c7ebd4565b95fb99983484cec76952f1505a75eb1a006b3ad9b8aa91a51"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'open' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
},
{
"ordinal": 19,
"name": "tax_transaction_version?",
"type_info": "Int4"
},
{
"ordinal": 20,
"name": "tax_platform_accounting_time?",
"type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d"
"hash": "4ed57832b7c02e1f4c683e256455c76e645cde49f95b0e5bfecd3d3d2330ed5c"
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss, tax_transaction_version, tax_platform_accounting_time)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n parent_charge_id = EXCLUDED.parent_charge_id,\n net = EXCLUDED.net,\n tax_amount = EXCLUDED.tax_amount,\n tax_platform_id = EXCLUDED.tax_platform_id,\n tax_last_updated = EXCLUDED.tax_last_updated,\n price_id = EXCLUDED.price_id,\n amount = EXCLUDED.amount,\n currency_code = EXCLUDED.currency_code,\n charge_type = EXCLUDED.charge_type,\n\t\t\t\t\ttax_drift_loss = EXCLUDED.tax_drift_loss,\n\t\t\t\t\ttax_transaction_version = EXCLUDED.tax_transaction_version,\n\t\t\t\t\ttax_platform_accounting_time = EXCLUDED.tax_platform_accounting_time\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Int8",
"Text",
"Text",
"Varchar",
"Timestamptz",
"Timestamptz",
"Int8",
"Text",
"Text",
"Text",
"Int8",
"Int8",
"Int8",
"Text",
"Timestamptz",
"Int8",
"Int4",
"Timestamptz"
]
},
"nullable": []
},
"hash": "5a972c49ccacf8735ec36d691f1c34b86c33703f984c869346b0c0be1c4a4883"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE id = $1",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE parent_charge_id = $1",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
},
{
"ordinal": 19,
"name": "tax_transaction_version?",
"type_info": "Int4"
},
{
"ordinal": 20,
"name": "tax_platform_accounting_time?",
"type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04"
"hash": "64233913683d187ee6c449eb106bd1a27929e05b497aaea93e9e8f318770c64c"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE affiliate_codes\n SET created_by = $1\n WHERE created_by = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "6443da83032ef5d6cb907f97fb37ae62351eeeb2ae3b8148cf8a8fd0deb2795a"
}

View File

@@ -0,0 +1,27 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users_subscriptions_credits\n (subscription_id, user_id, creditor_id, days, previous_due, next_due)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Int4",
"Timestamptz",
"Timestamptz"
]
},
"nullable": [
false
]
},
"hash": "68968755bd5eacce9009d1d873d08ef679e6638e57e4711c2c215f32e691d856"
}

View File

@@ -0,0 +1,143 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n AND payment_platform_id IS NOT NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n OFFSET $1\n\t\t\tLIMIT $2\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "price_id",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "amount",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "currency_code",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "status",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "due",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "last_attempt",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "charge_type",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "subscription_id",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "tax_amount",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "tax_platform_id",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "subscription_interval?",
"type_info": "Text"
},
{
"ordinal": 13,
"name": "payment_platform",
"type_info": "Text"
},
{
"ordinal": 14,
"name": "payment_platform_id?",
"type_info": "Text"
},
{
"ordinal": 15,
"name": "parent_charge_id?",
"type_info": "Int8"
},
{
"ordinal": 16,
"name": "net?",
"type_info": "Int8"
},
{
"ordinal": 17,
"name": "tax_last_updated?",
"type_info": "Timestamptz"
},
{
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
},
{
"ordinal": 19,
"name": "tax_transaction_version?",
"type_info": "Int4"
},
{
"ordinal": 20,
"name": "tax_platform_accounting_time?",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "70236e8be98967070160f703ed0242239eb5a4c6bef3748dac57fa339260c9c1"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM payouts_values_notifications\n WHERE user_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "713034d4968b290a0096e41b9da044f6760683cb89ff39255a177bb025e7638e"
}

View File

@@ -0,0 +1,142 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '30 days' < NOW() -- Due between 7 and 30 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "price_id",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "amount",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "currency_code",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "status",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "due",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "last_attempt",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "charge_type",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "subscription_id",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "tax_amount",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "tax_platform_id",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "subscription_interval?",
"type_info": "Text"
},
{
"ordinal": 13,
"name": "payment_platform",
"type_info": "Text"
},
{
"ordinal": 14,
"name": "payment_platform_id?",
"type_info": "Text"
},
{
"ordinal": 15,
"name": "parent_charge_id?",
"type_info": "Int8"
},
{
"ordinal": 16,
"name": "net?",
"type_info": "Int8"
},
{
"ordinal": 17,
"name": "tax_last_updated?",
"type_info": "Timestamptz"
},
{
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
},
{
"ordinal": 19,
"name": "tax_transaction_version?",
"type_info": "Int4"
},
{
"ordinal": 20,
"name": "tax_platform_accounting_time?",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "7d8de27065490edc560b0c81061295ca82f44546527e1a31c03e5bb7a07c1e63"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n charges.tax_transaction_version AS \"tax_transaction_version?\",\n charges.tax_platform_accounting_time AS \"tax_platform_accounting_time?\"\n FROM charges\n WHERE\n\t\t\t subscription_id = $1\n\t\t\t AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')\n\t\t\tORDER BY due ASC LIMIT 1",
"describe": {
"columns": [
{
@@ -97,6 +97,16 @@
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
},
{
"ordinal": 19,
"name": "tax_transaction_version?",
"type_info": "Int4"
},
{
"ordinal": 20,
"name": "tax_platform_accounting_time?",
"type_info": "Timestamptz"
}
],
"parameters": {
@@ -123,8 +133,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6"
"hash": "91866517bf34fb8bf31a7a49832b18fca60c293ad349eaec07b573d22a28301c"
}

View File

@@ -1,130 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "price_id",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "amount",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "currency_code",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "status",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "due",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "last_attempt",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "charge_type",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "subscription_id",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "tax_amount",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "tax_platform_id",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "subscription_interval?",
"type_info": "Text"
},
{
"ordinal": 13,
"name": "payment_platform",
"type_info": "Text"
},
{
"ordinal": 14,
"name": "payment_platform_id?",
"type_info": "Text"
},
{
"ordinal": 15,
"name": "parent_charge_id?",
"type_info": "Int8"
},
{
"ordinal": 16,
"name": "net?",
"type_info": "Int8"
},
{
"ordinal": 17,
"name": "tax_last_updated?",
"type_info": "Timestamptz"
},
{
"ordinal": 18,
"name": "tax_drift_loss?",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
true,
true,
true,
true,
true
]
},
"hash": "9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca"
}

View File

@@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata\n FROM users_subscriptions us\n WHERE us.metadata->>'type' = 'pyro' AND us.metadata->>'id' = ANY($1::text[])\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "price_id",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "interval",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "created",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "status",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "metadata",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"TextArray"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "aafa63e08f9d556dcd55c4687f0323aa502ce9feb3f439a614bb7759dcb03b23"
}

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