Compare commits

..
Author SHA1 Message Date
Josiah GlossonandAlejandro González ee9d4fad33 Add quotes around $env:JAVA_HOME_11_X64
Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com>
Signed-off-by: Josiah Glosson <soujournme@gmail.com>
2025-06-27 10:29:10 -05:00
Josiah Glosson 140b4060e6 Set JAVA_HOME to JAVA_HOME_11_X64 on Windows for theseus-release 2025-06-26 16:51:46 -05:00
Emma Alexia 6f03fae233 Clear owner's project cache after deleting organization (#3794)
* Clear owner's project cache after deleting organization

Fixes an issue where people would think their projects were deleted along with their organization, when this isn't actually the case.

* address PR review

* Update apps/labrinth/src/routes/v3/organizations.rs

Signed-off-by: Emma Alexia <wafflecoffee7@gmail.com>

* Fix lint

* actually fix lint

---------

Signed-off-by: Emma Alexia <wafflecoffee7@gmail.com>
2025-06-26 19:30:58 +00:00
Emma Alexia 22fc0c994d Remove new-projects channel emojis (#3843)
* Remove new-projects channel emojis

With new loaders, this functionality has become unwieldy. We don't have enough emoji slots in the server for the number of emojis we'd need for the loaders. It is easiest to simply format them in the same way knossos does.

Note: I forgor how the borrow checker works, this compiles but I'm sure it's gore to anyone who actually knows the difference between a string slice and a String, I come from Javaland though so pls forgive

* Rename func accordingly
2025-06-26 18:54:22 +00:00
Josiah Glosson a1ccbc5757 Update Java to 11 with --release 8 (#3846) 2025-06-26 18:38:58 +00:00
Jai A 053cf10198 Update ads.txt again
(cherry picked from commit 67304fab25ed37ed3e39766dc9378ae3a760cb06)
2025-06-26 11:08:44 -07:00
Jai A 257efd8ad7 Update ads.txt
(cherry picked from commit 01037c184efad97e4b9f1a11c3e7e1eb21d44510)
2025-06-26 11:08:44 -07:00
Alejandro González b75cfc063b Sign Windows Theseus binaries with DigiCert KeyLocker's cloud HSM (#3838)
* feat(ci): sign Windows Theseus bins with DigiCert KeyLocker cloud HSM

* perf(ci): speed up Jsign installation

* fix(ci): use absolute path to DigiCert client certificate

This should avoid errors related to Jsign not being able to find it
we've seen on CI.

* fix(ci): trim strange characters out from DigiCert credentials

* ci: another attempt at fixing Jsign errors

* chore: add comment mentioning why `jsign` choco deps are ignored

* tweak: move KeyLocker signing config to CI release Tauri config file

This prevents casual local builds from attempting to use a signing
command they really can't use, improving developer experience.

* tweak(ci/windows): do not waste time and signatures with MSIs

We aren't distributing these anyway. This should reduce the signing
operations required for building the app from 5 (one for the binary,
another for the MSI installer, two for WiX extension DLLs and one for
the NSIS installer) to 2.

* feat(ci): make Windows code signing toggleable, do not sign non-final builds

* chore(ci): tweak `sign-windows-binaries` input wording

* fix(ci): deal with usual Powershell syntax shenanigans

* fix(ci): work around more Powershell syntax shenanigans

Who thought it'd be a good idea to make a comma a synonymous of a space
for separating command line arguments? Why have to characters for the
same thing?

* perf(ci): do not run app build workflow on Labrinth changes

Labrinth is not related to the app at all, so this is just a waste of CI
minutes.

* ci(theseus): enable Windows code signing by default for manual triggers

These are expected to be not that common, so defaulting to what causes
the least human errors when it comes to publishing a release makes most
sense.
2025-06-26 17:43:20 +00:00
Prospector 2d8420131d Update changelog 2025-06-26 10:45:48 -07:00
Prospector c793b68aed Add quick server button, dynamic price preview for custom server modal (#3815)
* Add quick server creation button, and dynamic pricing to custom server selection

* Remove test in compatibility card

* Lint + remove duplicate file

* Adjust z-index of popup

* $6 -> $5

* Dismiss prompt if the button is clicked

* Make "Create a server" disabled for now

* Use existing loaders type
2025-06-26 15:38:42 +00:00
Josiah GlossonandAlejandro González 47af459f24 Migrate Java code to Gradle (#3833)
* Create get_resource_file macro to get an embedded resource

If the tauri feature is enabled, the resource will be loaded from Tauri resources.
If the tauri feature is disabled, the resource will be extracted to a temp directory.

* Wrap process execution to inject system properties through stdin

* Pass the time values as ISO 8601 datetimes

* Remove entirely internal modrinth.process.uuid

* Redo Java version checking somewhat and fix a few bugs with it

* Fix game launch with early access versions of Java

* Format Java code

* Use Gradle to build Java code

* Make Gradle build reproducible

* Fix constant rebuilds

* Get rid of unnecessary rebuilds

* Fix modrinth.profile.modified being the same as modrinth.profile.created

* Make javac use all lints and treat them as errors

* Force Gradle color output

* Add Java formatting config

* Make gradlew executable

* Revert to manually extracting class files

* Switch to using update resource macro

* fix: make `app-lib` build again

---------

Co-authored-by: Alejandro González <me@alegon.dev>
2025-06-26 14:56:35 +00:00
Josiah Glosson f10e0f2bf1 Pass system properties into Minecraft (+ some launch code cleanup) (#3822)
* Create get_resource_file macro to get an embedded resource

If the tauri feature is enabled, the resource will be loaded from Tauri resources.
If the tauri feature is disabled, the resource will be extracted to a temp directory.

* Wrap process execution to inject system properties through stdin

* Pass the time values as ISO 8601 datetimes

* Remove entirely internal modrinth.process.uuid

* Redo Java version checking somewhat and fix a few bugs with it

* Fix game launch with early access versions of Java

* Format Java code

* Fix modrinth.profile.modified being the same as modrinth.profile.created

* Revert to manually extracting class files
2025-06-26 13:23:14 +00:00
Alejandro González 569d60cb57 tweak(labrinth): return proper error message for invalid password change flows (#3839) 2025-06-25 16:51:08 +00:00
Ken 74d36a6a2d Update config.toml (#3832)
Signed-off-by: Ken <131881470+Keniis0712@users.noreply.github.com>
2025-06-23 22:40:16 +00:00
Prospector ced073d26c Add colors for new loaders, reduce utility redundancy (#3820)
* Add colors to some newer loaders

* Make loader formatting consistent everywhere, remove redundant utilities
2025-06-21 14:35:42 +00:00
Josiah Glosson cc34e69524 Initial shared instances backend (#3800)
* Create base shared instance migration and initial routes

* Fix build

* Add version uploads

* Add permissions field for shared instance users

* Actually use permissions field

* Add "public" flag to shared instances that allow GETing them without authorization

* Add the ability to get and list shared instance versions

* Add the ability to delete shared instance versions

* Fix build after merge

* Secured file hosting (#3784)

* Remove Backblaze-specific file-hosting backend

* Added S3_USES_PATH_STYLE_BUCKETS

* Remove unused file_id parameter from delete_file_version

* Add support for separate public and private buckets in labrinth::file_hosting

* Rename delete_file_version to delete_file

* Add (untested) get_url_for_private_file

* Remove url field from shared instance routes

* Remove url field from shared instance routes

* Use private bucket for shared instance versions

* Make S3 environment variables fully separate between public and private buckets

* Change file host expiry for shared instances to 180 seconds

* Fix lint

* Merge shared instance migrations into a single migration

* Replace shared instance owners with Ghost instead of deleting the instance
2025-06-19 19:46:12 +00:00
IMB11 d4864deac5 fix: intercom bubble colliding with notifications (#3810) 2025-06-19 15:18:10 +00:00
IMB11 125207880d fix: state update race conditons (#3812) 2025-06-19 15:18:00 +00:00
IMB11 a8f17f40f5 fix: unclear file upload errors temp fix (#3811) 2025-06-19 15:17:50 +00:00
Prospector dbde3c4669 Remove duplicate components in web frontend Avatar, Badge, CopyCode, and Pagination (#3741) 2025-06-19 00:07:15 +00:00
Josiah Glosson ba4fecb0cb Fix direct lint of packages/app-lib (#3808)
* Fix theseus lint by adding "unstable" feature to Tauri

* Remove "unstable" feature from tauri in apps/app
2025-06-18 20:49:23 +00:00
Alejandro González ef04dcc37b feat(labrinth): rework v3 side types to a single environment field (#3701)
* feat(labrinth): rework v3 side types to a single `environment` field

This field is meant to be able to represent the existing v2 side type
information and beyond, in a way that may also be slightly easier to
comprehend.

* chore(labrinth/migrations): use proper val for `HAVING` clause

* feat(labrinth): add `side_types_migration_review_status` field to projects
2025-06-16 22:44:57 +00:00
Prospector 65126b3a23 Update changelog again 2025-06-16 11:00:15 -07:00
Prospector 58495e6276 Update changelog 2025-06-16 10:59:01 -07:00
IMB11andProspector 706976439d fix: error handling improvements (#3797)
* fix: error handling improvements

* refactor: error info cards

* refactor: PyroError -> ModrinthError

* fix: lint

* fix: idiot

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-06-16 17:34:18 +00:00
jade 0a9ffd3dc8 fix(moderation): Insufficient gallery images moderation message indentation (#3799) 2025-06-16 16:48:35 +00:00
Alejandro González fb30c0ba2b feat(labrinth): allow protected resource and data packs to pass validation (#3792)
* fix(labrinth): return version artifact size exceeded error eagerly

Now we don't wait until the result memory buffer has grown to a size
greater than the maximum allowed, and instead we return such an error
before the buffer is grown with the current chunk, which should reduce
memory usage.

* fix(labrinth): proper supported game versions range for datapacks

* feat(labrinth): allow protected resource and data packs to pass validation
2025-06-16 16:30:01 +00:00
Alejandro González 97e4d8e132 fix(labrinth): ensure versions get removed from search indexes before ending route execution (#3789)
* fix(labrinth): ensure versions get removed from search indexes before ending route execution

* chore: run `sqlx prepare`

* chore(labrinth): simplify `remove_documents` a little

* chore: tweak new comment
2025-06-16 15:48:04 +00:00
Emma Alexia 5bdff3929b Allow failed subscriptions to be cancelled (#3795)
When a payment for a subscription fails, we continue to try to re-attempt retrieving payment for 30 days.

Sometimes making it fail is an intentional choice on the user's part (e.g. Privacy.com card) or other times the user just doesn't want their subscription anymore after it fails.

This PR allows users with a failed payment to simply cancel instead of waiting for the 30-day timer to set in.
2025-06-16 05:41:07 +00:00
Prospector a08562bfe2 Update changelog 2025-06-15 16:19:20 -07:00
Prospector 2b4319ea55 Servers hotfixes (#3793)
* servers: Fix installing modpacks from search

* remove console.log

* Fix subdomain setting
2025-06-15 16:17:38 -07:00
Prospector c32405720d Fix changelog 2025-06-15 14:39:19 -07:00
Alejandro González b9ba3cd3e8 fix(labrinth): use a proper CDN_URL for local deployments (#3791) 2025-06-15 15:19:56 +00:00
Prospector 31381c860b Update changelog 2025-06-14 10:47:19 -07:00
230 changed files with 5815 additions and 4816 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler # Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
[target.'cfg(windows)'] [target.'cfg(windows)']
rustflags = ["-C", "link-args=/STACK:16777220"] rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
[build] [build]
rustflags = ["--cfg", "tokio_unstable"] rustflags = ["--cfg", "tokio_unstable"]
+2 -2
View File
@@ -14,5 +14,5 @@ max_line_length = 100
max_line_length = off max_line_length = off
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.rs] [*.{rs,java,kts}]
indent_size = 4 indent_size = 4
+36 -7
View File
@@ -9,14 +9,18 @@ on:
- .github/workflows/theseus-release.yml - .github/workflows/theseus-release.yml
- 'apps/app/**' - 'apps/app/**'
- 'apps/app-frontend/**' - 'apps/app-frontend/**'
- 'apps/labrinth/src/common/**'
- 'apps/labrinth/Cargo.toml'
- 'packages/app-lib/**' - 'packages/app-lib/**'
- 'packages/app-macros/**' - 'packages/app-macros/**'
- 'packages/assets/**' - 'packages/assets/**'
- 'packages/ui/**' - 'packages/ui/**'
- 'packages/utils/**' - 'packages/utils/**'
workflow_dispatch: workflow_dispatch:
inputs:
sign-windows-binaries:
description: Sign Windows binaries
type: boolean
default: true
required: false
jobs: jobs:
build: build:
@@ -103,11 +107,21 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
- name: Install code signing client (Windows only)
if: startsWith(matrix.platform, 'windows')
run: choco install jsign --ignore-dependencies # GitHub runners come with a global Java installation already
- name: Install frontend dependencies - name: Install frontend dependencies
run: pnpm install run: pnpm install
- name: Disable Windows code signing for non-final release builds
if: ${{ startsWith(matrix.platform, 'windows') && !startsWith(github.ref, 'refs/tags/v') && !inputs.sign-windows-binaries }}
run: |
jq 'del(.bundle.windows.signCommand)' apps/app/tauri-release.conf.json > apps/app/tauri-release.conf.json.new
Move-Item -Path apps/app/tauri-release.conf.json.new -Destination apps/app/tauri-release.conf.json -Force
- name: build app (macos) - name: build app (macos)
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config "tauri-release.conf.json" run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
if: startsWith(matrix.platform, 'macos') if: startsWith(matrix.platform, 'macos')
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -121,15 +135,30 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: build app - name: build app (Linux)
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json" run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
id: build_os if: startsWith(matrix.platform, 'ubuntu')
if: "!startsWith(matrix.platform, 'macos')"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: build app (Windows)
run: |
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
Remove-Item -Path signer-client-cert.p12
if: startsWith(matrix.platform, 'windows')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
DIGICERT_ONE_SIGNER_API_KEY: ${{ secrets.DIGICERT_ONE_SIGNER_API_KEY }}
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
- name: upload ${{ matrix.platform }} - name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
+1 -1
View File
@@ -17,4 +17,4 @@
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>
+1 -1
View File
@@ -5,4 +5,4 @@
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
</modules> </modules>
</component> </component>
</project> </project>
Generated
+1 -1
View File
@@ -8889,7 +8889,7 @@ dependencies = [
"flate2", "flate2",
"fs4", "fs4",
"futures", "futures",
"heck 0.5.0", "hashlink",
"hickory-resolver", "hickory-resolver",
"indicatif", "indicatif",
"notify", "notify",
+1 -1
View File
@@ -60,7 +60,7 @@ flate2 = "1.1.2"
fs4 = { version = "0.13.1", default-features = false } fs4 = { version = "0.13.1", default-features = false }
futures = { version = "0.3.31", default-features = false } futures = { version = "0.3.31", default-features = false }
futures-util = "0.3.31" futures-util = "0.3.31"
heck = "0.5.0" hashlink = "0.10.0"
hex = "0.4.3" hex = "0.4.3"
hickory-resolver = "0.25.2" hickory-resolver = "0.25.2"
hmac = "0.12.1" hmac = "0.12.1"
@@ -10,12 +10,12 @@
size="36px" size="36px"
:src=" :src="
selectedAccount selectedAccount
? `https://mc-heads.net/avatar/${selectedAccount.profile.id}/128` ? `https://mc-heads.net/avatar/${selectedAccount.id}/128`
: 'https://launcher-files.modrinth.com/assets/steve_head.png' : 'https://launcher-files.modrinth.com/assets/steve_head.png'
" "
/> />
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span> <span>{{ selectedAccount ? selectedAccount.username : 'Select account' }}</span>
<span class="text-secondary text-xs">Minecraft account</span> <span class="text-secondary text-xs">Minecraft account</span>
</div> </div>
<DropdownIcon class="w-5 h-5 shrink-0" /> <DropdownIcon class="w-5 h-5 shrink-0" />
@@ -28,17 +28,12 @@
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }" :class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
> >
<div v-if="selectedAccount" class="selected account"> <div v-if="selectedAccount" class="selected account">
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`" /> <Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.id}/128`" />
<div> <div>
<h4>{{ selectedAccount.profile.name }}</h4> <h4>{{ selectedAccount.username }}</h4>
<p>Selected</p> <p>Selected</p>
</div> </div>
<Button <Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
v-tooltip="'Log out'"
icon-only
color="raised"
@click="logout(selectedAccount.profile.id)"
>
<TrashIcon /> <TrashIcon />
</Button> </Button>
</div> </div>
@@ -49,12 +44,12 @@
</Button> </Button>
</div> </div>
<div v-if="displayAccounts.length > 0" class="account-group"> <div v-if="displayAccounts.length > 0" class="account-group">
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row"> <div v-for="account in displayAccounts" :key="account.id" class="account-row">
<Button class="option account" @click="setAccount(account)"> <Button class="option account" @click="setAccount(account)">
<Avatar :src="`https://mc-heads.net/avatar/${account.profile.id}/128`" class="icon" /> <Avatar :src="`https://mc-heads.net/avatar/${account.id}/128`" class="icon" />
<p>{{ account.profile.name }}</p> <p>{{ account.username }}</p>
</Button> </Button>
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)"> <Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
<TrashIcon /> <TrashIcon />
</Button> </Button>
</div> </div>
@@ -106,16 +101,16 @@ defineExpose({
await refreshValues() await refreshValues()
const displayAccounts = computed(() => const displayAccounts = computed(() =>
accounts.value.filter((account) => defaultUser.value !== account.profile.id), accounts.value.filter((account) => defaultUser.value !== account.id),
) )
const selectedAccount = computed(() => const selectedAccount = computed(() =>
accounts.value.find((account) => account.profile.id === defaultUser.value), accounts.value.find((account) => account.id === defaultUser.value),
) )
async function setAccount(account) { async function setAccount(account) {
defaultUser.value = account.profile.id defaultUser.value = account.id
await set_default_user(account.profile.id).catch(handleError) await set_default_user(account.id).catch(handleError)
emit('change') emit('change')
} }
@@ -92,7 +92,7 @@ async function loginMinecraft() {
const loggedIn = await login_flow() const loggedIn = await login_flow()
if (loggedIn) { if (loggedIn) {
await set_default_user(loggedIn.profile.id).catch(handleError) await set_default_user(loggedIn.id).catch(handleError)
} }
await trackEvent('AccountLogIn', { source: 'ErrorModal' }) await trackEvent('AccountLogIn', { source: 'ErrorModal' })
@@ -127,7 +127,7 @@ async function handleJavaFileInput() {
const filePath = await open() const filePath = await open()
if (filePath) { if (filePath) {
let result = await get_jre(filePath.path ?? filePath) let result = await get_jre(filePath.path ?? filePath).catch(handleError)
if (!result) { if (!result) {
result = { result = {
path: filePath.path ?? filePath, path: filePath.path ?? filePath,
+1 -4
View File
@@ -27,10 +27,7 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
let credentials = minecraft_auth::finish_login(&input, login).await?; let credentials = minecraft_auth::finish_login(&input, login).await?;
println!( println!("Logged in user {}.", credentials.username);
"Logged in user {}.",
credentials.maybe_online_profile().await.name
);
Ok(credentials) Ok(credentials)
} }
+1 -1
View File
@@ -16,7 +16,7 @@ serde_json.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_with.workspace = true serde_with.workspace = true
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] } tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset"] }
tauri-plugin-window-state.workspace = true tauri-plugin-window-state.workspace = true
tauri-plugin-deep-link.workspace = true tauri-plugin-deep-link.workspace = true
tauri-plugin-os.workspace = true tauri-plugin-os.workspace = true
+1
View File
@@ -151,6 +151,7 @@ fn main() {
"profile_update_managed_modrinth_version", "profile_update_managed_modrinth_version",
"profile_repair_managed_modrinth", "profile_repair_managed_modrinth",
"profile_run", "profile_run",
"profile_run_credentials",
"profile_kill", "profile_kill",
"profile_edit", "profile_edit",
"profile_edit_icon", "profile_edit_icon",
+2 -2
View File
@@ -41,8 +41,8 @@ pub async fn jre_find_filtered_jres(
// Validates JRE at a given path // Validates JRE at a given path
// Returns None if the path is not a valid JRE // Returns None if the path is not a valid JRE
#[tauri::command] #[tauri::command]
pub async fn jre_get_jre(path: PathBuf) -> Result<Option<JavaVersion>> { pub async fn jre_get_jre(path: PathBuf) -> Result<JavaVersion> {
jre::check_jre(path).await.map_err(|e| e.into()) Ok(jre::check_jre(path).await?)
} }
// Tests JRE of a certain version // Tests JRE of a certain version
+17
View File
@@ -28,6 +28,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
profile_update_managed_modrinth_version, profile_update_managed_modrinth_version,
profile_repair_managed_modrinth, profile_repair_managed_modrinth,
profile_run, profile_run,
profile_run_credentials,
profile_kill, profile_kill,
profile_edit, profile_edit,
profile_edit_icon, profile_edit_icon,
@@ -255,6 +256,22 @@ pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
Ok(process) Ok(process)
} }
// Run Minecraft using a profile using chosen credentials
// Returns the UUID, which can be used to poll
// for the actual Child in the state.
// invoke('plugin:profile|profile_run_credentials', {path, credentials})')
#[tauri::command]
pub async fn profile_run_credentials(
path: &str,
credentials: Credentials,
) -> Result<ProcessMetadata> {
let process =
profile::run_credentials(path, &credentials, &QuickPlayType::None)
.await?;
Ok(process)
}
#[tauri::command] #[tauri::command]
pub async fn profile_kill(path: &str) -> Result<()> { pub async fn profile_kill(path: &str) -> Result<()> {
profile::kill(path).await?; profile::kill(path).await?;
+19 -1
View File
@@ -1,6 +1,24 @@
{ {
"bundle": { "bundle": {
"createUpdaterArtifacts": "v1Compatible" "createUpdaterArtifacts": "v1Compatible",
"windows": {
"signCommand": {
"cmd": "jsign",
"args": [
"sign",
"--verbose",
"--storetype",
"DIGICERTONE",
"--keystore",
"https://clientauth.one.digicert.com",
"--storepass",
"env:DIGICERT_ONE_SIGNER_CREDENTIALS",
"--tsaurl",
"https://timestamp.sectigo.com,http://timestamp.digicert.com",
"%1"
]
}
}
}, },
"build": { "build": {
"features": ["updater"] "features": ["updater"]
-4
View File
@@ -14,9 +14,6 @@
"externalBin": [], "externalBin": [],
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"windows": { "windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com",
"nsis": { "nsis": {
"installMode": "perMachine", "installMode": "perMachine",
"installerHooks": "./nsis/hooks.nsi" "installerHooks": "./nsis/hooks.nsi"
@@ -30,7 +27,6 @@
"providerShortName": null, "providerShortName": null,
"signingIdentity": null "signingIdentity": null
}, },
"resources": [],
"shortDescription": "", "shortDescription": "",
"linux": { "linux": {
"deb": { "deb": {
@@ -85,11 +85,10 @@ During development, you might notice that changes made directly to entities in t
#### CDN options #### CDN options
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local`, `backblaze`, or `s3`, but defaults to `local` `STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local` or `s3`, but defaults to `local`
The Backblaze and S3 configuration options are fairly self-explanatory in name, so here's simply their names: The S3 configuration options are fairly self-explanatory in name, so here's simply their names:
`BACKBLAZE_KEY_ID`, `BACKBLAZE_KEY`, `BACKBLAZE_BUCKET_ID` `S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_PUBLIC_BUCKET_NAME`, `S3_PRIVATE_BUCKET_NAME`, `S3_USES_PATH_STYLE_BUCKETS`
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_BUCKET_NAME`
#### Search, OAuth, and miscellaneous options #### Search, OAuth, and miscellaneous options
-1
View File
@@ -2,4 +2,3 @@ BASE_URL=http://127.0.0.1:8000/v2/
BROWSER_BASE_URL=http://127.0.0.1:8000/v2/ BROWSER_BASE_URL=http://127.0.0.1:8000/v2/
PYRO_BASE_URL=https://staging-archon.modrinth.com PYRO_BASE_URL=https://staging-archon.modrinth.com
PROD_OVERRIDE=true PROD_OVERRIDE=true
@@ -1,46 +0,0 @@
<template>
<OmorphiaAvatar
:src="src"
:alt="alt"
:size="size"
:circle="circle"
:no-shadow="noShadow"
:loading="loading"
:raised="raised"
/>
</template>
<script setup>
import { Avatar as OmorphiaAvatar } from "@modrinth/ui";
const props = defineProps({
src: {
type: String,
default: null,
},
alt: {
type: String,
default: "",
},
size: {
type: String,
default: "2rem",
},
circle: {
type: Boolean,
default: false,
},
noShadow: {
type: Boolean,
default: false,
},
loading: {
type: String,
default: "eager",
},
raised: {
type: Boolean,
default: false,
},
});
</script>
-131
View File
@@ -1,131 +0,0 @@
<template>
<span
:class="
'badge flex items-center gap-1 font-semibold text-secondary ' + color + ' type--' + type
"
>
<template v-if="color"> <span class="circle" /> {{ capitalizeString(type) }}</template>
<!-- User roles -->
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team</template>
<template v-else-if="type === 'moderator'"> <ModeratorIcon /> Moderator</template>
<template v-else-if="type === 'creator'"><CreatorIcon /> Creator</template>
<template v-else-if="type === 'plus'"><PlusIcon /> Modrinth Plus</template>
<!-- Project statuses -->
<template v-else-if="type === 'approved'"><GlobeIcon /> Public</template>
<template v-else-if="type === 'approved-general'"><CheckIcon /> Approved</template>
<template v-else-if="type === 'unlisted' || type === 'withheld'"
><LinkIcon /> Unlisted</template
>
<template v-else-if="type === 'private'"><LockIcon /> Private</template>
<template v-else-if="type === 'scheduled'"> <CalendarIcon /> Scheduled</template>
<template v-else-if="type === 'draft'"><DraftIcon /> Draft</template>
<template v-else-if="type === 'archived'"> <ArchiveIcon /> Archived</template>
<template v-else-if="type === 'rejected'"><CrossIcon /> Rejected</template>
<template v-else-if="type === 'processing'"> <ProcessingIcon /> Under review</template>
<!-- Team members -->
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
<template v-else-if="type === 'pending'"> <ProcessingIcon /> Pending </template>
<!-- Transaction statuses -->
<template v-else-if="type === 'success'"><CheckIcon /> Success</template>
<!-- Report status -->
<template v-else-if="type === 'closed'"> <CloseIcon /> Closed</template>
<!-- Other -->
<template v-else> <span class="circle" /> {{ capitalizeString(type) }} </template>
</span>
</template>
<script setup>
import {
GlobeIcon,
LinkIcon,
ModrinthIcon,
PlusIcon,
ScaleIcon as ModeratorIcon,
BoxIcon as CreatorIcon,
FileTextIcon as DraftIcon,
XIcon as CrossIcon,
ArchiveIcon,
UpdatedIcon as ProcessingIcon,
CheckIcon,
LockIcon,
CalendarIcon,
XCircleIcon as CloseIcon,
} from "@modrinth/assets";
import { capitalizeString } from "@modrinth/utils";
defineProps({
type: {
type: String,
required: true,
},
color: {
type: String,
default: "",
},
});
</script>
<style lang="scss" scoped>
.badge {
.circle {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
display: inline-block;
margin-right: 0.25rem;
background-color: var(--badge-color);
}
svg {
vertical-align: -15%;
width: 1em;
height: 1em;
}
&.type--closed,
&.type--withheld,
&.type--rejected,
&.red {
--badge-color: var(--color-red);
}
&.type--pending,
&.type--moderator,
&.type--processing,
&.type--scheduled,
&.orange {
--badge-color: var(--color-orange);
}
&.type--accepted,
&.type--admin,
&.type--success,
&.type--approved-general,
&.green {
--badge-color: var(--color-green);
}
&.type--creator,
&.blue {
--badge-color: var(--color-blue);
}
&.type--unlisted,
&.type--plus,
&.purple {
--badge-color: var(--color-purple);
}
&.type--private,
&.type--approved,
&.gray {
--badge-color: var(--color-secondary);
}
}
</style>
@@ -1,75 +0,0 @@
<template>
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
<span>{{ text }}</span>
<CheckIcon v-if="copied" />
<ClipboardCopyIcon v-else />
</button>
</template>
<script>
import { CheckIcon, ClipboardCopyIcon } from "@modrinth/assets";
export default {
components: {
CheckIcon,
ClipboardCopyIcon,
},
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
copied: false,
};
},
methods: {
async copyText() {
await navigator.clipboard.writeText(this.text);
this.copied = true;
},
},
};
</script>
<style lang="scss" scoped>
.code {
color: var(--color-text);
display: inline-flex;
grid-gap: 0.5rem;
font-family: var(--mono-font);
font-size: var(--font-size-sm);
margin: 0;
padding: 0.25rem 0.5rem;
background-color: var(--color-code-bg);
width: fit-content;
border-radius: 10px;
user-select: text;
transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
transform 0.05s ease-in-out,
outline 0.2s ease-in-out;
span {
overflow: hidden;
text-overflow: ellipsis;
}
svg {
width: 1em;
height: 1em;
}
&:hover {
filter: brightness(0.85);
}
&:active {
transform: scale(0.95);
filter: brightness(0.8);
}
}
</style>
@@ -654,11 +654,11 @@ For a brief rundown of how this works:
{ {
name: "Insufficient", name: "Insufficient",
resultingMessage: `## Insufficient Gallery Images resultingMessage: `## Insufficient Gallery Images
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations). We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations).
Keep in mind that you should: Keep in mind that you should:
- Set a featured image that best represents your project. - Set a featured image that best represents your project.
- Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description. - Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description.
- Upload any relevant images in your Description to your Gallery tab for best results.`, - Upload any relevant images in your Description to your Gallery tab for best results.`,
}, },
{ {
name: "Not relevant", name: "Not relevant",
@@ -104,13 +104,13 @@
</nuxt-link> </nuxt-link>
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)"> <template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
has been has been
<Badge :type="notification.body.new_status" /> <ProjectStatusBadge :status="notification.body.new_status" />
</template> </template>
<template v-else> <template v-else>
updated from updated from
<Badge :type="notification.body.old_status" /> <ProjectStatusBadge :status="notification.body.old_status" />
to to
<Badge :type="notification.body.new_status" /> <ProjectStatusBadge :status="notification.body.new_status" />
</template> </template>
by the moderators. by the moderators.
</template> </template>
@@ -331,16 +331,13 @@ import {
XIcon, XIcon,
ExternalIcon, ExternalIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { useRelativeTime } from "@modrinth/ui"; import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue"; import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import { getProjectLink, getVersionLink } from "~/helpers/projects.js"; import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
import { getUserLink } from "~/helpers/users.js"; import { getUserLink } from "~/helpers/users.js";
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js"; import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
import { markAsRead } from "~/helpers/notifications.ts"; import { markAsRead } from "~/helpers/notifications.ts";
import DoubleIcon from "~/components/ui/DoubleIcon.vue"; import DoubleIcon from "~/components/ui/DoubleIcon.vue";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import Categories from "~/components/ui/search/Categories.vue"; import Categories from "~/components/ui/search/Categories.vue";
const app = useNuxtApp(); const app = useNuxtApp();
@@ -1,5 +1,8 @@
<template> <template>
<div class="vue-notification-group experimental-styles-within"> <div
class="vue-notification-group experimental-styles-within"
:class="{ 'intercom-present': isIntercomPresent }"
>
<transition-group name="notifs"> <transition-group name="notifs">
<div <div
v-for="(item, index) in notifications" v-for="(item, index) in notifications"
@@ -80,6 +83,8 @@ import {
} from "@modrinth/assets"; } from "@modrinth/assets";
const notifications = useNotifications(); const notifications = useNotifications();
const isIntercomPresent = ref(false);
function stopTimer(notif) { function stopTimer(notif) {
clearTimeout(notif.timer); clearTimeout(notif.timer);
} }
@@ -106,6 +111,27 @@ const createNotifText = (notif) => {
return text; return text;
}; };
function checkIntercomPresence() {
isIntercomPresent.value = !!document.querySelector(".intercom-lightweight-app");
}
onMounted(() => {
checkIntercomPresence();
const observer = new MutationObserver(() => {
checkIntercomPresence();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
onBeforeUnmount(() => {
observer.disconnect();
});
});
function copyToClipboard(notif) { function copyToClipboard(notif) {
const text = createNotifText(notif); const text = createNotifText(notif);
@@ -130,6 +156,10 @@ function copyToClipboard(notif) {
bottom: 0.75rem; bottom: 0.75rem;
} }
&.intercom-present {
bottom: 5rem;
}
.vue-notification-wrapper { .vue-notification-wrapper {
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
@@ -57,7 +57,7 @@
<div class="table-cell"> <div class="table-cell">
<BoxIcon /> <BoxIcon />
<span>{{ <span>{{
$formatProjectType( formatProjectType(
$getProjectTypeForDisplay( $getProjectTypeForDisplay(
project.project_types?.[0] ?? "project", project.project_types?.[0] ?? "project",
project.loaders, project.loaders,
@@ -111,6 +111,7 @@
<script setup> <script setup>
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets"; import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui"; import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils";
const modalOpen = ref(null); const modalOpen = ref(null);
@@ -1,196 +0,0 @@
<template>
<div v-if="count > 1" class="columns paginates">
<a
:class="{ disabled: page === 1 }"
:tabindex="page === 1 ? -1 : 0"
class="left-arrow paginate has-icon"
aria-label="Previous Page"
:href="linkFunction(page - 1)"
@click.prevent="page !== 1 ? switchPage(page - 1) : null"
>
<LeftArrowIcon />
</a>
<div
v-for="(item, index) in pages"
:key="'page-' + item + '-' + index"
:class="{
'page-number': page !== item,
shrink: item > 99,
}"
class="page-number-container"
>
<div v-if="item === '-'" class="has-icon">
<GapIcon />
</div>
<a
v-else
:class="{
'page-number current': page === item,
shrink: item > 99,
}"
:href="linkFunction(item)"
@click.prevent="page !== item ? switchPage(item) : null"
>
{{ item }}
</a>
</div>
<a
:class="{
disabled: page === pages[pages.length - 1],
}"
:tabindex="page === pages[pages.length - 1] ? -1 : 0"
class="right-arrow paginate has-icon"
aria-label="Next Page"
:href="linkFunction(page + 1)"
@click.prevent="page !== pages[pages.length - 1] ? switchPage(page + 1) : null"
>
<RightArrowIcon />
</a>
</div>
</template>
<script>
import { GapIcon, LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
export default {
components: {
GapIcon,
LeftArrowIcon,
RightArrowIcon,
},
props: {
page: {
type: Number,
default: 1,
},
count: {
type: Number,
default: 1,
},
linkFunction: {
type: Function,
default() {
return () => "/";
},
},
},
emits: ["switch-page"],
computed: {
pages() {
let pages = [];
if (this.count > 7) {
if (this.page + 3 >= this.count) {
pages = [
1,
"-",
this.count - 4,
this.count - 3,
this.count - 2,
this.count - 1,
this.count,
];
} else if (this.page > 5) {
pages = [1, "-", this.page - 1, this.page, this.page + 1, "-", this.count];
} else {
pages = [1, 2, 3, 4, 5, "-", this.count];
}
} else {
pages = Array.from({ length: this.count }, (_, i) => i + 1);
}
return pages;
},
},
methods: {
switchPage(newPage) {
this.$emit("switch-page", newPage);
if (newPage !== null && newPage !== "" && !isNaN(newPage)) {
this.$emit("switch-page", Math.min(Math.max(newPage, 1), this.count));
}
},
},
};
</script>
<style scoped lang="scss">
a {
position: relative;
color: var(--color-button-text);
box-shadow: var(--shadow-raised), var(--shadow-inset);
padding: 0.5rem 1rem;
margin: 0;
border-radius: 2rem;
background: var(--color-raised-bg);
transition:
opacity 0.5s ease-in-out,
filter 0.2s ease-in-out,
transform 0.05s ease-in-out,
outline 0.2s ease-in-out;
&.page-number.current {
background: var(--color-brand);
color: var(--color-brand-inverted);
cursor: default;
outline: 2px solid transparent;
}
&.paginate.disabled {
background-color: transparent;
cursor: not-allowed;
filter: grayscale(50%);
opacity: 0.5;
}
}
.has-icon {
display: flex;
align-items: center;
svg {
width: 1em;
}
}
.page-number-container,
a,
.has-icon {
display: flex;
justify-content: center;
align-items: center;
}
.paginates {
height: 2em;
margin: 0.5rem 0;
> div,
.has-icon {
margin: 0 0.3em;
}
}
.left-arrow {
margin-left: auto !important;
}
.right-arrow {
margin-right: auto !important;
}
@media screen and (max-width: 400px) {
.paginates {
font-size: 80%;
}
}
@media screen and (max-width: 530px) {
a {
width: 2.5rem;
padding: 0.5rem 0;
}
}
</style>
@@ -29,7 +29,7 @@
{{ author }} {{ author }}
</nuxt-link> </nuxt-link>
</p> </p>
<Badge v-if="status && status !== 'approved'" :type="status" class="status" /> <ProjectStatusBadge v-if="status && status !== 'approved'" :status="status" class="status" />
</div> </div>
<p class="description"> <p class="description">
{{ description }} {{ description }}
@@ -91,18 +91,16 @@
<script> <script>
import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets"; import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets";
import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
import Categories from "~/components/ui/search/Categories.vue"; import Categories from "~/components/ui/search/Categories.vue";
import Badge from "~/components/ui/Badge.vue";
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue"; import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
import Avatar from "~/components/ui/Avatar.vue";
import { useRelativeTime } from "@modrinth/ui";
export default { export default {
components: { components: {
ProjectStatusBadge,
EnvironmentIndicator, EnvironmentIndicator,
Avatar, Avatar,
Categories, Categories,
Badge,
CalendarIcon, CalendarIcon,
UpdatedIcon, UpdatedIcon,
DownloadIcon, DownloadIcon,
@@ -118,7 +118,7 @@ import {
ScaleIcon, ScaleIcon,
DropdownIcon, DropdownIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { formatProjectType } from "~/plugins/shorthands.js"; import { formatProjectType } from "@modrinth/utils";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js"; import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
const props = defineProps({ const props = defineProps({
@@ -11,8 +11,8 @@
<div class="stacked"> <div class="stacked">
<span class="title">{{ report.project.title }}</span> <span class="title">{{ report.project.title }}</span>
<span>{{ <span>{{
$formatProjectType( formatProjectType(
$getProjectTypeForUrl(report.project.project_type, report.project.loaders), getProjectTypeForUrl(report.project.project_type, report.project.loaders),
) )
}}</span> }}</span>
</div> </div>
@@ -53,8 +53,8 @@
<div class="stacked"> <div class="stacked">
<span class="title">{{ report.project.title }}</span> <span class="title">{{ report.project.title }}</span>
<span>{{ <span>{{
$formatProjectType( formatProjectType(
$getProjectTypeForUrl(report.project.project_type, report.project.loaders), getProjectTypeForUrl(report.project.project_type, report.project.loaders),
) )
}}</span> }}</span>
</div> </div>
@@ -104,12 +104,11 @@
<script setup> <script setup>
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets"; import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils";
import { renderHighlightedString } from "~/helpers/highlight.js"; import { renderHighlightedString } from "~/helpers/highlight.js";
import { useRelativeTime } from "@modrinth/ui";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue"; import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
import CopyCode from "~/components/ui/CopyCode.vue"; import { getProjectTypeForUrl } from "~/helpers/projects.js";
const formatRelativeTime = useRelativeTime(); const formatRelativeTime = useRelativeTime();
@@ -4,12 +4,14 @@
<span <span
v-for="category in categoriesFiltered" v-for="category in categoriesFiltered"
:key="category.name" :key="category.name"
v-html="category.icon + $formatCategory(category.name)" v-html="category.icon + formatCategory(category.name)"
/> />
</div> </div>
</template> </template>
<script> <script>
import { formatCategory } from "@modrinth/utils";
export default { export default {
props: { props: {
categories: { categories: {
@@ -38,6 +40,7 @@ export default {
); );
}, },
}, },
methods: { formatCategory },
}; };
</script> </script>
@@ -16,7 +16,7 @@ import {
} from "@modrinth/assets"; } from "@modrinth/assets";
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui"; import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
import { defineMessages, useVIntl } from "@vintl/vintl"; import { defineMessages, useVIntl } from "@vintl/vintl";
import { ref } from "vue"; import { ref, computed } from "vue";
import type { Backup } from "@modrinth/utils"; import type { Backup } from "@modrinth/utils";
const flags = useFeatureFlags(); const flags = useFeatureFlags();
@@ -52,9 +52,10 @@ const failedToCreate = computed(() => props.backup.interrupted);
const preparedDownloadStates = ["ready", "done"]; const preparedDownloadStates = ["ready", "done"];
const inactiveStates = ["failed", "cancelled"]; const inactiveStates = ["failed", "cancelled"];
const hasPreparedDownload = computed(() => const hasPreparedDownload = computed(() => {
preparedDownloadStates.includes(props.backup.task?.file?.state ?? ""), const fileState = props.backup.task?.file?.state ?? "";
); return preparedDownloadStates.includes(fileState);
});
const creating = computed(() => { const creating = computed(() => {
const task = props.backup.task?.create; const task = props.backup.task?.create;
@@ -81,6 +82,10 @@ const restoring = computed(() => {
const initiatedPrepare = ref(false); const initiatedPrepare = ref(false);
const preparingFile = computed(() => { const preparingFile = computed(() => {
if (hasPreparedDownload.value) {
return false;
}
const task = props.backup.task?.file; const task = props.backup.task?.file;
return ( return (
(!task && initiatedPrepare.value) || (!task && initiatedPrepare.value) ||
@@ -39,7 +39,7 @@
/> />
<XCircleIcon <XCircleIcon
v-show=" v-show="
item.status === 'error' || item.status.includes('error') ||
item.status === 'cancelled' || item.status === 'cancelled' ||
item.status === 'incorrect-type' item.status === 'incorrect-type'
" "
@@ -54,9 +54,14 @@
<template v-if="item.status === 'completed'"> <template v-if="item.status === 'completed'">
<span>Done</span> <span>Done</span>
</template> </template>
<template v-else-if="item.status === 'error'"> <template v-else-if="item.status === 'error-file-exists'">
<span class="text-red">Failed - File already exists</span> <span class="text-red">Failed - File already exists</span>
</template> </template>
<template v-else-if="item.status === 'error-generic'">
<span class="text-red"
>Failed - {{ item.error?.message || "An unexpected error occured." }}</span
>
</template>
<template v-else-if="item.status === 'incorrect-type'"> <template v-else-if="item.status === 'incorrect-type'">
<span class="text-red">Failed - Incorrect file type</span> <span class="text-red">Failed - Incorrect file type</span>
</template> </template>
@@ -104,9 +109,17 @@ import { FSModule } from "~/composables/servers/modules/fs.ts";
interface UploadItem { interface UploadItem {
file: File; file: File;
progress: number; progress: number;
status: "pending" | "uploading" | "completed" | "error" | "cancelled" | "incorrect-type"; status:
| "pending"
| "uploading"
| "completed"
| "error-file-exists"
| "error-generic"
| "cancelled"
| "incorrect-type";
size: string; size: string;
uploader?: any; uploader?: any;
error?: Error;
} }
interface Props { interface Props {
@@ -245,8 +258,18 @@ const uploadFile = async (file: File) => {
console.error("Error uploading file:", error); console.error("Error uploading file:", error);
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name); const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") { if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
uploadQueue.value[index].status = const target = uploadQueue.value[index];
error instanceof Error && error.message === badFileTypeMsg ? "incorrect-type" : "error";
if (error instanceof Error) {
if (error.message === badFileTypeMsg) {
target.status = "incorrect-type";
} else if (target.progress === 100 && error.message.includes("401")) {
target.status = "error-file-exists";
} else {
target.status = "error-generic";
target.error = error;
}
}
} }
setTimeout(async () => { setTimeout(async () => {
@@ -214,7 +214,7 @@
</template> </template>
<script setup> <script setup>
import { OverflowMenu, MarkdownEditor } from "@modrinth/ui"; import { CopyCode, OverflowMenu, MarkdownEditor } from "@modrinth/ui";
import { import {
DropdownIcon, DropdownIcon,
ReplyIcon, ReplyIcon,
@@ -226,7 +226,6 @@ import {
ScaleIcon, ScaleIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { useImageUpload } from "~/composables/image-upload.ts"; import { useImageUpload } from "~/composables/image-upload.ts";
import CopyCode from "~/components/ui/CopyCode.vue";
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue"; import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
import { isStaff } from "~/helpers/users.js"; import { isStaff } from "~/helpers/users.js";
import { isApproved, isRejected } from "~/helpers/projects.js"; import { isApproved, isRejected } from "~/helpers/projects.js";
@@ -103,10 +103,8 @@ import {
ModrinthIcon, ModrinthIcon,
ScaleIcon, ScaleIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { AutoLink, OverflowMenu, useRelativeTime } from "@modrinth/ui"; import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui";
import { renderString } from "@modrinth/utils"; import { renderString } from "@modrinth/utils";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
import { isStaff } from "~/helpers/users.js"; import { isStaff } from "~/helpers/users.js";
const props = defineProps({ const props = defineProps({
@@ -31,6 +31,9 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
projectBackground: false, projectBackground: false,
searchBackground: false, searchBackground: false,
advancedDebugInfo: false, advancedDebugInfo: false,
showProjectPageDownloadModalServersPromo: true,
showProjectPageCreateServersTooltip: true,
showProjectPageQuickServerButton: false,
// advancedRendering: true, // advancedRendering: true,
// externalLinksNewTab: true, // externalLinksNewTab: true,
// notUsingBlockers: false, // notUsingBlockers: false,
@@ -124,58 +124,63 @@ export class ModrinthServer {
return dataURL; return dataURL;
} }
} catch (error) { } catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) { if (error instanceof ModrinthServerError && error.statusCode === 404) {
// Handle external icon processing if (iconUrl) {
try { try {
const response = await fetch(iconUrl); const response = await fetch(iconUrl);
if (!response.ok) throw new Error("Failed to fetch icon"); if (!response.ok) throw new Error("Failed to fetch icon");
const file = await response.blob(); const file = await response.blob();
const originalFile = new File([file], "server-icon-original.png", { const originalFile = new File([file], "server-icon-original.png", {
type: "image/png", type: "image/png",
});
if (import.meta.client) {
const dataURL = await new Promise<string>((resolve) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = 64;
canvas.height = 64;
ctx?.drawImage(img, 0, 0, 64, 64);
canvas.toBlob(async (blob) => {
if (blob) {
const scaledFile = new File([blob], "server-icon.png", { type: "image/png" });
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: scaledFile,
override: auth,
});
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: originalFile,
override: auth,
});
}
}, "image/png");
const dataURL = canvas.toDataURL("image/png");
sharedImage.value = dataURL;
resolve(dataURL);
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
}); });
return dataURL;
if (import.meta.client) {
const dataURL = await new Promise<string>((resolve) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = 64;
canvas.height = 64;
ctx?.drawImage(img, 0, 0, 64, 64);
canvas.toBlob(async (blob) => {
if (blob) {
const scaledFile = new File([blob], "server-icon.png", {
type: "image/png",
});
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: scaledFile,
override: auth,
});
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: originalFile,
override: auth,
});
}
}, "image/png");
const dataURL = canvas.toDataURL("image/png");
sharedImage.value = dataURL;
resolve(dataURL);
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
});
return dataURL;
}
} catch (externalError: any) {
console.debug("Could not process external icon:", externalError.message);
} }
} catch (error) {
console.error("Failed to process external icon:", error);
} }
} else {
throw error;
} }
} }
} catch (error) { } catch (error: any) {
console.error("Failed to process server icon:", error); console.debug("Icon processing failed:", error.message);
} }
sharedImage.value = undefined; sharedImage.value = undefined;
@@ -239,6 +244,18 @@ export class ModrinthServer {
break; break;
} }
} catch (error) { } catch (error) {
if (error instanceof ModrinthServerError) {
if (error.statusCode === 404 && ["fs", "content"].includes(module)) {
console.debug(`Optional ${module} resource not found:`, error.message);
continue;
}
if (error.statusCode === 503) {
console.debug(`Temporary ${module} unavailable:`, error.message);
continue;
}
}
this.errors[module] = { this.errors[module] = {
error: error:
error instanceof ModrinthServerError error instanceof ModrinthServerError
@@ -155,19 +155,25 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
} }
async setMotd(motd: string): Promise<void> { async setMotd(motd: string): Promise<void> {
const props = (await this.server.fetchConfigFile("ServerProperties")) as any; try {
if (props) { const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
props.motd = motd; if (props) {
const newProps = this.server.constructServerProperties(props); props.motd = motd;
const octetStream = new Blob([newProps], { type: "application/octet-stream" }); const newProps = this.server.constructServerProperties(props);
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`); const octetStream = new Blob([newProps], { type: "application/octet-stream" });
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
await useServersFetch(`/update?path=/server.properties`, { await useServersFetch(`/update?path=/server.properties`, {
method: "PUT", method: "PUT",
contentType: "application/octet-stream", contentType: "application/octet-stream",
body: octetStream, body: octetStream,
override: auth, override: auth,
}); });
}
} catch {
console.error(
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
);
} }
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { formatBytes } from "~/plugins/shorthands.js"; import { formatBytes } from "@modrinth/utils";
export const fileIsValid = (file, validationOptions) => { export const fileIsValid = (file, validationOptions) => {
const { maxSize, alertOnInvalid } = validationOptions; const { maxSize, alertOnInvalid } = validationOptions;
+118 -4
View File
@@ -32,7 +32,7 @@
<h1 class="wrap-as-needed"> <h1 class="wrap-as-needed">
{{ project.title }} {{ project.title }}
</h1> </h1>
<Badge :type="project.status" /> <ProjectStatusBadge :status="project.status" />
</div> </div>
</div> </div>
<h2>Project settings</h2> <h2>Project settings</h2>
@@ -452,6 +452,16 @@
{{ formatCategory(currentPlatform) }}. {{ formatCategory(currentPlatform) }}.
</p> </p>
</AutomaticAccordion> </AutomaticAccordion>
<ServersPromo
v-if="flags.showProjectPageDownloadModalServersPromo"
:link="`/servers#plan`"
@close="
() => {
flags.showProjectPageDownloadModalServersPromo = false;
saveFeatureFlags();
}
"
/>
</div> </div>
</template> </template>
</NewModal> </NewModal>
@@ -495,6 +505,64 @@
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </div>
<Tooltip
v-if="canCreateServerFrom && flags.showProjectPageQuickServerButton"
theme="dismissable-prompt"
:triggers="[]"
:shown="flags.showProjectPageCreateServersTooltip"
:auto-hide="false"
placement="bottom-start"
>
<ButtonStyled size="large" circular>
<nuxt-link
v-tooltip="'Create a server'"
:to="`/servers?project=${project.id}#plan`"
@click="
() => {
flags.showProjectPageCreateServersTooltip = false;
saveFeatureFlags();
}
"
>
<ServerPlusIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<template #popper>
<div class="experimental-styles-within flex max-w-60 flex-col gap-1">
<div class="flex items-center justify-between gap-4">
<h3 class="m-0 flex items-center gap-2 text-base font-bold text-contrast">
Create a server
<TagItem
:style="{
'--_color': 'var(--color-brand)',
'--_bg-color': 'var(--color-brand-highlight)',
}"
>New</TagItem
>
</h3>
<ButtonStyled size="small" circular>
<button
v-tooltip="`Don't show again`"
@click="
() => {
flags.showProjectPageCreateServersTooltip = false;
saveFeatureFlags();
}
"
>
<XIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
Modrinth Servers is the easiest way to play with your friends without hassle!
</p>
<p class="m-0 text-wrap text-sm font-bold text-primary">
Starting at $5<span class="text-xs"> / month</span>
</p>
</div>
</template>
</Tooltip>
<ClientOnly> <ClientOnly>
<ButtonStyled <ButtonStyled
size="large" size="large"
@@ -850,12 +918,14 @@ import {
ReportIcon, ReportIcon,
ScaleIcon, ScaleIcon,
SearchIcon, SearchIcon,
ServerPlusIcon,
SettingsIcon, SettingsIcon,
TagsIcon, TagsIcon,
UsersIcon, UsersIcon,
VersionIcon, VersionIcon,
WrenchIcon, WrenchIcon,
ModrinthIcon, ModrinthIcon,
XIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
Avatar, Avatar,
@@ -870,17 +940,27 @@ import {
ProjectSidebarCreators, ProjectSidebarCreators,
ProjectSidebarDetails, ProjectSidebarDetails,
ProjectSidebarLinks, ProjectSidebarLinks,
ProjectStatusBadge,
ScrollablePanel, ScrollablePanel,
TagItem,
ServersPromo,
useRelativeTime, useRelativeTime,
} from "@modrinth/ui"; } from "@modrinth/ui";
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue"; import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils"; import {
formatCategory,
formatProjectType,
isRejected,
isStaff,
isUnderReview,
renderString,
} from "@modrinth/utils";
import { navigateTo } from "#app"; import { navigateTo } from "#app";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Tooltip } from "floating-vue";
import Accordion from "~/components/ui/Accordion.vue"; import Accordion from "~/components/ui/Accordion.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue"; import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
import Badge from "~/components/ui/Badge.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue"; import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import MessageBanner from "~/components/ui/MessageBanner.vue"; import MessageBanner from "~/components/ui/MessageBanner.vue";
@@ -891,6 +971,7 @@ import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue"; import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
import { userCollectProject } from "~/composables/user.js"; import { userCollectProject } from "~/composables/user.js";
import { reportProject } from "~/utils/report-helpers.ts"; import { reportProject } from "~/utils/report-helpers.ts";
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
const data = useNuxtApp(); const data = useNuxtApp();
const route = useNativeRoute(); const route = useNativeRoute();
@@ -1286,7 +1367,7 @@ featuredVersions.value.sort((a, b) => {
}); });
const projectTypeDisplay = computed(() => const projectTypeDisplay = computed(() =>
data.$formatProjectType( formatProjectType(
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders), data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
), ),
); );
@@ -1304,6 +1385,10 @@ const description = computed(
} by ${members.value.find((x) => x.is_owner)?.user?.username || "a Creator"} on Modrinth`, } by ${members.value.find((x) => x.is_owner)?.user?.username || "a Creator"} on Modrinth`,
); );
const canCreateServerFrom = computed(() => {
return project.value.project_type === "modpack" && project.value.server_side !== "unsupported";
});
if (!route.name.startsWith("type-id-settings")) { if (!route.name.startsWith("type-id-settings")) {
useSeoMeta({ useSeoMeta({
title: () => title.value, title: () => title.value,
@@ -1672,4 +1757,33 @@ const navLinks = computed(() => {
display: none; display: none;
} }
} }
.servers-popup {
box-shadow:
0 0 12px 1px rgba(0, 175, 92, 0.6),
var(--shadow-floating);
&::before {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid var(--color-button-bg);
content: " ";
position: absolute;
top: -7px;
left: 17px;
}
&::after {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid var(--color-raised-bg);
content: " ";
position: absolute;
top: -5px;
left: 18px;
}
}
</style> </style>
@@ -104,7 +104,7 @@
<span class="label__title">Client-side</span> <span class="label__title">Client-side</span>
<span class="label__description"> <span class="label__description">
Select based on if the Select based on if the
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the {{ formatProjectType(project.project_type).toLowerCase() }} has functionality on the
client side. Just because a mod works in Singleplayer doesn't mean it has actual client side. Just because a mod works in Singleplayer doesn't mean it has actual
client-side functionality. client-side functionality.
</span> </span>
@@ -128,7 +128,7 @@
<span class="label__title">Server-side</span> <span class="label__title">Server-side</span>
<span class="label__description"> <span class="label__description">
Select based on if the Select based on if the
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the {{ formatProjectType(project.project_type).toLowerCase() }} has functionality on the
<strong>logical</strong> server. Remember that Singleplayer contains an integrated <strong>logical</strong> server. Remember that Singleplayer contains an integrated
server. server.
</span> </span>
@@ -239,11 +239,10 @@
</template> </template>
<script setup> <script setup>
import { formatProjectStatus } from "@modrinth/utils"; import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets"; import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
import { Multiselect } from "vue-multiselect"; import { Multiselect } from "vue-multiselect";
import { ConfirmModal } from "@modrinth/ui"; import { ConfirmModal, Avatar } from "@modrinth/ui";
import Avatar from "~/components/ui/Avatar.vue";
import FileInput from "~/components/ui/FileInput.vue"; import FileInput from "~/components/ui/FileInput.vue";
const props = defineProps({ const props = defineProps({
@@ -8,7 +8,7 @@
</div> </div>
<p> <p>
Accurate tagging is important to help people find your Accurate tagging is important to help people find your
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags {{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
that apply. that apply.
</p> </p>
<p v-if="project.versions.length === 0" class="known-errors"> <p v-if="project.versions.length === 0" class="known-errors">
@@ -18,25 +18,25 @@
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`"> <template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
<div class="label"> <div class="label">
<h4> <h4>
<span class="label__title">{{ $formatCategoryHeader(header) }}</span> <span class="label__title">{{ formatCategoryHeader(header) }}</span>
</h4> </h4>
<span class="label__description"> <span class="label__description">
<template v-if="header === 'categories'"> <template v-if="header === 'categories'">
Select all categories that reflect the themes or function of your Select all categories that reflect the themes or function of your
{{ $formatProjectType(project.project_type).toLowerCase() }}. {{ formatProjectType(project.project_type).toLowerCase() }}.
</template> </template>
<template v-else-if="header === 'features'"> <template v-else-if="header === 'features'">
Select all of the features that your Select all of the features that your
{{ $formatProjectType(project.project_type).toLowerCase() }} makes use of. {{ formatProjectType(project.project_type).toLowerCase() }} makes use of.
</template> </template>
<template v-else-if="header === 'resolutions'"> <template v-else-if="header === 'resolutions'">
Select the resolution(s) of textures in your Select the resolution(s) of textures in your
{{ $formatProjectType(project.project_type).toLowerCase() }}. {{ formatProjectType(project.project_type).toLowerCase() }}.
</template> </template>
<template v-else-if="header === 'performance impact'"> <template v-else-if="header === 'performance impact'">
Select the realistic performance impact of your Select the realistic performance impact of your
{{ $formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the {{ formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
{{ $formatProjectType(project.project_type).toLowerCase() }} is configurable to {{ formatProjectType(project.project_type).toLowerCase() }} is configurable to
different levels of performance impact. different levels of performance impact.
</template> </template>
</span> </span>
@@ -46,7 +46,7 @@
v-for="category in categoryLists[header]" v-for="category in categoryLists[header]"
:key="`category-${header}-${category.name}`" :key="`category-${header}-${category.name}`"
:model-value="selectedTags.includes(category)" :model-value="selectedTags.includes(category)"
:description="$formatCategory(category.name)" :description="formatCategory(category.name)"
class="category-selector" class="category-selector"
@update:model-value="toggleCategory(category)" @update:model-value="toggleCategory(category)"
> >
@@ -57,7 +57,7 @@
class="icon" class="icon"
v-html="category.icon" v-html="category.icon"
/> />
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span> <span aria-hidden="true"> {{ formatCategory(category.name) }}</span>
</div> </div>
</Checkbox> </Checkbox>
</div> </div>
@@ -80,7 +80,7 @@
:key="`featured-category-${category.name}`" :key="`featured-category-${category.name}`"
class="category-selector" class="category-selector"
:model-value="featuredTags.includes(category)" :model-value="featuredTags.includes(category)"
:description="$formatCategory(category.name)" :description="formatCategory(category.name)"
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)" :disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
@update:model-value="toggleFeaturedCategory(category)" @update:model-value="toggleFeaturedCategory(category)"
> >
@@ -91,7 +91,7 @@
class="icon" class="icon"
v-html="category.icon" v-html="category.icon"
/> />
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span> <span aria-hidden="true"> {{ formatCategory(category.name) }}</span>
</div> </div>
</Checkbox> </Checkbox>
</div> </div>
@@ -114,6 +114,7 @@
<script> <script>
import { StarIcon, SaveIcon } from "@modrinth/assets"; import { StarIcon, SaveIcon } from "@modrinth/assets";
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils";
import Checkbox from "~/components/ui/Checkbox.vue"; import Checkbox from "~/components/ui/Checkbox.vue";
export default defineNuxtComponent({ export default defineNuxtComponent({
@@ -222,6 +223,9 @@ export default defineNuxtComponent({
}, },
}, },
methods: { methods: {
formatProjectType,
formatCategoryHeader,
formatCategory,
toggleCategory(category) { toggleCategory(category) {
if (this.selectedTags.includes(category)) { if (this.selectedTags.includes(category)) {
this.selectedTags = this.selectedTags.filter((x) => x !== category); this.selectedTags = this.selectedTags.filter((x) => x !== category);
@@ -144,7 +144,7 @@
<div v-else class="input-group"> <div v-else class="input-group">
<ButtonStyled v-if="primaryFile" color="brand"> <ButtonStyled v-if="primaryFile" color="brand">
<a <a
v-tooltip="primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'" v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
:href="primaryFile.url" :href="primaryFile.url"
@click="emit('onDownload')" @click="emit('onDownload')"
> >
@@ -320,7 +320,7 @@
<FileIcon aria-hidden="true" /> <FileIcon aria-hidden="true" />
<span class="filename"> <span class="filename">
<strong>{{ replaceFile.name }}</strong> <strong>{{ replaceFile.name }}</strong>
<span class="file-size">({{ $formatBytes(replaceFile.size) }})</span> <span class="file-size">({{ formatBytes(replaceFile.size) }})</span>
</span> </span>
<FileInput <FileInput
class="iconified-button raised-button" class="iconified-button raised-button"
@@ -345,7 +345,7 @@
<FileIcon aria-hidden="true" /> <FileIcon aria-hidden="true" />
<span class="filename"> <span class="filename">
<strong>{{ file.filename }}</strong> <strong>{{ file.filename }}</strong>
<span class="file-size">({{ $formatBytes(file.size) }})</span> <span class="file-size">({{ formatBytes(file.size) }})</span>
<span v-if="primaryFile.hashes.sha1 === file.hashes.sha1" class="file-type"> <span v-if="primaryFile.hashes.sha1 === file.hashes.sha1" class="file-type">
Primary Primary
</span> </span>
@@ -412,7 +412,7 @@
<FileIcon aria-hidden="true" /> <FileIcon aria-hidden="true" />
<span class="filename"> <span class="filename">
<strong>{{ file.name }}</strong> <strong>{{ file.name }}</strong>
<span class="file-size">({{ $formatBytes(file.size) }})</span> <span class="file-size">({{ formatBytes(file.size) }})</span>
</span> </span>
<multiselect <multiselect
v-if="version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))" v-if="version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))"
@@ -533,7 +533,7 @@
) )
.map((it) => it.name) .map((it) => it.name)
" "
:custom-label="(value) => $formatCategory(value)" :custom-label="formatCategory"
:loading="tags.loaders.length === 0" :loading="tags.loaders.length === 0"
:multiple="true" :multiple="true"
:searchable="true" :searchable="true"
@@ -630,7 +630,15 @@
</div> </div>
</template> </template>
<script> <script>
import { ButtonStyled, ConfirmModal, MarkdownEditor } from "@modrinth/ui"; import {
Avatar,
Badge,
CopyCode,
Checkbox,
ButtonStyled,
ConfirmModal,
MarkdownEditor,
} from "@modrinth/ui";
import { import {
FileIcon, FileIcon,
TrashIcon, TrashIcon,
@@ -649,6 +657,7 @@ import {
ChevronRightIcon, ChevronRightIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Multiselect } from "vue-multiselect"; import { Multiselect } from "vue-multiselect";
import { formatBytes, formatCategory } from "@modrinth/utils";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js"; import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
import { inferVersionInfo } from "~/helpers/infer.js"; import { inferVersionInfo } from "~/helpers/infer.js";
import { createDataPackVersion } from "~/helpers/package.js"; import { createDataPackVersion } from "~/helpers/package.js";
@@ -656,13 +665,9 @@ import { renderHighlightedString } from "~/helpers/highlight.js";
import { reportVersion } from "~/utils/report-helpers.ts"; import { reportVersion } from "~/utils/report-helpers.ts";
import { useImageUpload } from "~/composables/image-upload.ts"; import { useImageUpload } from "~/composables/image-upload.ts";
import Avatar from "~/components/ui/Avatar.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
import Badge from "~/components/ui/Badge.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import Categories from "~/components/ui/search/Categories.vue"; import Categories from "~/components/ui/search/Categories.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import FileInput from "~/components/ui/FileInput.vue"; import FileInput from "~/components/ui/FileInput.vue";
import Modal from "~/components/ui/Modal.vue"; import Modal from "~/components/ui/Modal.vue";
@@ -958,6 +963,8 @@ export default defineNuxtComponent({
}, },
}, },
methods: { methods: {
formatBytes,
formatCategory,
async onImageUpload(file) { async onImageUpload(file) {
const response = await useImageUpload(file, { context: "version" }); const response = await useImageUpload(file, { context: "version" });
+1 -3
View File
@@ -8,13 +8,11 @@ import {
DownloadIcon, DownloadIcon,
LinkIcon, LinkIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import Avatar from "~/components/ui/Avatar.vue"; import { Avatar, Checkbox, Badge } from "@modrinth/ui";
import LogoAnimated from "~/components/brand/LogoAnimated.vue"; import LogoAnimated from "~/components/brand/LogoAnimated.vue";
import Badge from "~/components/ui/Badge.vue";
import PrismIcon from "~/assets/images/external/prism.svg?component"; import PrismIcon from "~/assets/images/external/prism.svg?component";
import ATLauncher from "~/assets/images/external/atlauncher.svg?component"; import ATLauncher from "~/assets/images/external/atlauncher.svg?component";
import CurseForge from "~/assets/images/external/curseforge.svg?component"; import CurseForge from "~/assets/images/external/curseforge.svg?component";
import Checkbox from "~/components/ui/Checkbox.vue";
import { homePageProjects } from "~/generated/state.json"; import { homePageProjects } from "~/generated/state.json";
+1 -1
View File
@@ -97,7 +97,7 @@
</template> </template>
<script setup> <script setup>
import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets"; import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets";
import Avatar from "~/components/ui/Avatar.vue"; import { Avatar } from "@modrinth/ui";
import NotificationItem from "~/components/ui/NotificationItem.vue"; import NotificationItem from "~/components/ui/NotificationItem.vue";
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts"; import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts";
@@ -26,7 +26,7 @@
v-if="notifTypes.length > 1" v-if="notifTypes.length > 1"
v-model="selectedType" v-model="selectedType"
:items="notifTypes" :items="notifTypes"
:format-label="(x) => (x === 'all' ? 'All' : $formatProjectType(x).replace('_', ' ') + 's')" :format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')"
:capitalize="false" :capitalize="false"
/> />
<p v-if="pending">Loading notifications...</p> <p v-if="pending">Loading notifications...</p>
@@ -49,13 +49,16 @@
/> />
</template> </template>
<p v-else>You don't have any unread notifications.</p> <p v-else>You don't have any unread notifications.</p>
<Pagination :page="page" :count="pages" @switch-page="changePage" /> <div class="flex justify-end">
<Pagination :page="page" :count="pages" @switch-page="changePage" />
</div>
</section> </section>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Button, Chips } from "@modrinth/ui"; import { Button, Pagination, Chips } from "@modrinth/ui";
import { HistoryIcon, CheckCheckIcon } from "@modrinth/assets"; import { HistoryIcon, CheckCheckIcon } from "@modrinth/assets";
import { formatProjectType } from "@modrinth/utils";
import { import {
fetchExtraNotificationData, fetchExtraNotificationData,
groupNotifications, groupNotifications,
@@ -63,7 +66,6 @@ import {
} from "~/helpers/notifications.ts"; } from "~/helpers/notifications.ts";
import NotificationItem from "~/components/ui/NotificationItem.vue"; import NotificationItem from "~/components/ui/NotificationItem.vue";
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue"; import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
import Pagination from "~/components/ui/Pagination.vue";
useHead({ useHead({
title: "Notifications - Modrinth", title: "Notifications - Modrinth",
+27 -18
View File
@@ -239,7 +239,7 @@
<div> <div>
<nuxt-link <nuxt-link
tabindex="-1" tabindex="-1"
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${ :to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}`" }`"
> >
@@ -261,7 +261,7 @@
<nuxt-link <nuxt-link
class="hover-link wrap-as-needed" class="hover-link wrap-as-needed"
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${ :to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}`" }`"
> >
@@ -275,22 +275,23 @@
</div> </div>
<div> <div>
{{ $formatProjectType($getProjectTypeForUrl(project.project_type, project.loaders)) }} {{ formatProjectType(getProjectTypeForUrl(project.project_type, project.loaders)) }}
</div> </div>
<div> <div>
<Badge v-if="project.status" :type="project.status" class="status" /> <ProjectStatusBadge v-if="project.status" :status="project.status" />
</div> </div>
<div> <div>
<nuxt-link <ButtonStyled circular>
class="square-button" <nuxt-link
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${ :to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}/settings`" }/settings`"
> >
<SettingsIcon /> <SettingsIcon />
</nuxt-link> </nuxt-link>
</ButtonStyled>
</div> </div>
</div> </div>
</div> </div>
@@ -312,19 +313,25 @@ import {
SortAscendingIcon as AscendingIcon, SortAscendingIcon as AscendingIcon,
SortDescendingIcon as DescendingIcon, SortDescendingIcon as DescendingIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { commonMessages } from "@modrinth/ui"; import {
Avatar,
ButtonStyled,
Checkbox,
CopyCode,
ProjectStatusBadge,
commonMessages,
} from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils";
import Badge from "~/components/ui/Badge.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import Modal from "~/components/ui/Modal.vue"; import Modal from "~/components/ui/Modal.vue";
import Avatar from "~/components/ui/Avatar.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import CopyCode from "~/components/ui/CopyCode.vue"; import { getProjectTypeForUrl } from "~/helpers/projects.js";
export default defineNuxtComponent({ export default defineNuxtComponent({
components: { components: {
Avatar, Avatar,
Badge, ButtonStyled,
ProjectStatusBadge,
SettingsIcon, SettingsIcon,
TrashIcon, TrashIcon,
Checkbox, Checkbox,
@@ -390,6 +397,8 @@ export default defineNuxtComponent({
this.DELETE_PROJECT = 1 << 7; this.DELETE_PROJECT = 1 << 7;
}, },
methods: { methods: {
getProjectTypeForUrl,
formatProjectType,
updateDescending() { updateDescending() {
this.descending = !this.descending; this.descending = !this.descending;
this.projects = this.updateSort(this.projects, this.sortBy, this.descending); this.projects = this.updateSort(this.projects, this.sortBy, this.descending);
@@ -76,7 +76,7 @@
</span> </span>
<template v-if="payout.method"> <template v-if="payout.method">
<span>⋅</span> <span>⋅</span>
<span>{{ $formatWallet(payout.method) }} ({{ payout.method_address }})</span> <span>{{ formatWallet(payout.method) }} ({{ payout.method_address }})</span>
</template> </template>
</div> </div>
</div> </div>
@@ -95,7 +95,7 @@
</template> </template>
<script setup> <script setup>
import { XIcon, PayPalIcon, UnknownIcon } from "@modrinth/assets"; import { XIcon, PayPalIcon, UnknownIcon } from "@modrinth/assets";
import { capitalizeString } from "@modrinth/utils"; import { capitalizeString, formatWallet } from "@modrinth/utils";
import { Badge, Breadcrumbs, DropdownSelect } from "@modrinth/ui"; import { Badge, Breadcrumbs, DropdownSelect } from "@modrinth/ui";
import dayjs from "dayjs"; import dayjs from "dayjs";
import TremendousIcon from "~/assets/images/external/tremendous.svg?component"; import TremendousIcon from "~/assets/images/external/tremendous.svg?component";
@@ -139,8 +139,8 @@
<template v-if="knownErrors.length === 0 && amount"> <template v-if="knownErrors.length === 0 && amount">
<Checkbox v-if="fees > 0" v-model="agreedFees" description="Consent to fee"> <Checkbox v-if="fees > 0" v-model="agreedFees" description="Consent to fee">
I acknowledge that an estimated I acknowledge that an estimated
{{ $formatMoney(fees) }} will be deducted from the amount I receive to cover {{ formatMoney(fees) }} will be deducted from the amount I receive to cover
{{ $formatWallet(selectedMethod.type) }} processing fees. {{ formatWallet(selectedMethod.type) }} processing fees.
</Checkbox> </Checkbox>
<Checkbox v-model="agreedTransfer" description="Confirm transfer"> <Checkbox v-model="agreedTransfer" description="Confirm transfer">
<template v-if="selectedMethod.type === 'tremendous'"> <template v-if="selectedMethod.type === 'tremendous'">
@@ -149,7 +149,7 @@
</template> </template>
<template v-else> <template v-else>
I confirm that I am initiating a transfer to the following I confirm that I am initiating a transfer to the following
{{ $formatWallet(selectedMethod.type) }} account: {{ withdrawAccount }} {{ formatWallet(selectedMethod.type) }} account: {{ withdrawAccount }}
</template> </template>
</Checkbox> </Checkbox>
<Checkbox v-model="agreedTerms" class="rewards-checkbox"> <Checkbox v-model="agreedTerms" class="rewards-checkbox">
@@ -198,6 +198,7 @@ import {
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Chips, Checkbox, Breadcrumbs } from "@modrinth/ui"; import { Chips, Checkbox, Breadcrumbs } from "@modrinth/ui";
import { all } from "iso-3166-1"; import { all } from "iso-3166-1";
import { formatMoney, formatWallet } from "@modrinth/utils";
import VenmoIcon from "~/assets/images/external/venmo.svg?component"; import VenmoIcon from "~/assets/images/external/venmo.svg?component";
const auth = await useAuth(); const auth = await useAuth();
@@ -360,9 +361,7 @@ async function withdraw() {
text: text:
selectedMethod.value.type === "tremendous" selectedMethod.value.type === "tremendous"
? "An email has been sent to your account with further instructions on how to redeem your payout!" ? "An email has been sent to your account with further instructions on how to redeem your payout!"
: `Payment has been sent to your ${data.$formatWallet( : `Payment has been sent to your ${formatWallet(selectedMethod.value.type)} account!`,
selectedMethod.value.type,
)} account!`,
type: "success", type: "success",
}); });
} catch (err) { } catch (err) {
+1 -2
View File
@@ -527,7 +527,7 @@
</template> </template>
<script setup> <script setup>
import { Multiselect } from "vue-multiselect"; import { Multiselect } from "vue-multiselect";
import { ButtonStyled, useRelativeTime } from "@modrinth/ui"; import { Avatar, ButtonStyled, useRelativeTime } from "@modrinth/ui";
import { import {
CompassIcon, CompassIcon,
LogInIcon, LogInIcon,
@@ -539,7 +539,6 @@ import {
} from "@modrinth/assets"; } from "@modrinth/assets";
import PrismLauncherLogo from "~/assets/images/external/prism.svg?component"; import PrismLauncherLogo from "~/assets/images/external/prism.svg?component";
import ATLauncherLogo from "~/assets/images/external/atlauncher.svg?component"; import ATLauncherLogo from "~/assets/images/external/atlauncher.svg?component";
import Avatar from "~/components/ui/Avatar.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json"; import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json";
+1 -1
View File
@@ -32,7 +32,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { formatNumber } from "~/plugins/shorthands.js"; import { formatNumber } from "@modrinth/utils";
useHead({ useHead({
title: "Staff overview - Modrinth", title: "Staff overview - Modrinth",
@@ -5,7 +5,7 @@
<Chips <Chips
v-model="projectType" v-model="projectType"
:items="projectTypes" :items="projectTypes"
:format-label="(x) => (x === 'all' ? 'All' : $formatProjectType(x) + 's')" :format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x) + 's')"
/> />
<button v-if="oldestFirst" class="iconified-button push-right" @click="oldestFirst = false"> <button v-if="oldestFirst" class="iconified-button push-right" @click="oldestFirst = false">
<SortDescendingIcon /> <SortDescendingIcon />
@@ -56,7 +56,7 @@
<Avatar :src="project.icon_url" size="xs" no-shadow raised /> <Avatar :src="project.icon_url" size="xs" no-shadow raised />
<span class="stacked"> <span class="stacked">
<span class="title">{{ project.name }}</span> <span class="title">{{ project.name }}</span>
<span>{{ $formatProjectType(project.inferred_project_type) }}</span> <span>{{ formatProjectType(project.inferred_project_type) }}</span>
</span> </span>
</nuxt-link> </nuxt-link>
</div> </div>
@@ -81,7 +81,9 @@
</div> </div>
<div class="mobile-row"> <div class="mobile-row">
is requesting to be is requesting to be
<Badge :type="project.requested_status ? project.requested_status : 'approved'" /> <ProjectStatusBadge
:status="project.requested_status ? project.requested_status : 'approved'"
/>
</div> </div>
</div> </div>
<div class="input-group"> <div class="input-group">
@@ -103,7 +105,7 @@
</template> </template>
<script setup> <script setup>
import { Chips, useRelativeTime } from "@modrinth/ui"; import { Avatar, ProjectStatusBadge, Chips, useRelativeTime } from "@modrinth/ui";
import { import {
UnknownIcon, UnknownIcon,
EyeIcon, EyeIcon,
@@ -112,9 +114,7 @@ import {
IssuesIcon, IssuesIcon,
ScaleIcon, ScaleIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import Avatar from "~/components/ui/Avatar.vue"; import { formatProjectType } from "@modrinth/utils";
import Badge from "~/components/ui/Badge.vue";
import { formatProjectType } from "~/plugins/shorthands.js";
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts"; import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
useHead({ useHead({
@@ -272,7 +272,7 @@
<div class="table-cell"> <div class="table-cell">
<BoxIcon /> <BoxIcon />
<span>{{ <span>{{
$formatProjectType( formatProjectType(
$getProjectTypeForDisplay(project.project_types[0] ?? "project", project.loaders), $getProjectTypeForDisplay(project.project_types[0] ?? "project", project.loaders),
) )
}}</span> }}</span>
@@ -313,6 +313,7 @@ import {
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Button, Modal, Avatar, CopyCode, Badge, Checkbox, commonMessages } from "@modrinth/ui"; import { Button, Modal, Avatar, CopyCode, Badge, Checkbox, commonMessages } from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import OrganizationProjectTransferModal from "~/components/ui/OrganizationProjectTransferModal.vue"; import OrganizationProjectTransferModal from "~/components/ui/OrganizationProjectTransferModal.vue";
@@ -520,7 +520,6 @@ async function serverInstall(project) {
if (projectType.value.id === "modpack") { if (projectType.value.id === "modpack") {
await server.value.general.reinstall( await server.value.general.reinstall(
server.value.serverId,
false, false,
project.project_id, project.project_id,
version.id, version.id,
+30 -6
View File
@@ -500,6 +500,7 @@
<section <section
id="plan" id="plan"
pyro-hash="plan"
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48" class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
> >
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div> <div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
@@ -603,7 +604,9 @@
<RightArrowIcon class="shrink-0" /> <RightArrowIcon class="shrink-0" />
</button> </button>
</ButtonStyled> </ButtonStyled>
<p class="m-0 text-sm">Starting at $3/GB RAM</p> <p v-if="lowestPrice" class="m-0 text-sm">
Starting at {{ formatPrice(locale, lowestPrice, selectedCurrency, true) }} / month
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -622,20 +625,34 @@ import {
VersionIcon, VersionIcon,
ServerIcon, ServerIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { computed } from "vue";
import { monthsInInterval } from "@modrinth/ui/src/utils/billing.ts";
import { formatPrice } from "@modrinth/utils";
import { useVIntl } from "@vintl/vintl";
import { products } from "~/generated/state.json"; import { products } from "~/generated/state.json";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts"; import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue"; import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue"; import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
import OptionGroup from "~/components/ui/OptionGroup.vue"; import OptionGroup from "~/components/ui/OptionGroup.vue";
const { locale } = useVIntl();
const billingPeriods = ref(["monthly", "quarterly"]); const billingPeriods = ref(["monthly", "quarterly"]);
const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly"); const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly");
const pyroProducts = products.filter((p) => p.metadata.type === "pyro"); const pyroProducts = products
.filter((p) => p.metadata.type === "pyro")
.sort((a, b) => a.metadata.ram - b.metadata.ram);
const pyroPlanProducts = pyroProducts.filter( const pyroPlanProducts = pyroProducts.filter(
(p) => p.metadata.ram === 4096 || p.metadata.ram === 6144 || p.metadata.ram === 8192, (p) => p.metadata.ram === 4096 || p.metadata.ram === 6144 || p.metadata.ram === 8192,
); );
pyroPlanProducts.sort((a, b) => a.metadata.ram - b.metadata.ram);
const lowestPrice = computed(() => {
const amount = pyroProducts[0]?.prices?.find(
(price) => price.currency_code === selectedCurrency.value,
)?.prices?.intervals?.[billingPeriod.value];
return amount ? amount / monthsInInterval[billingPeriod.value] : undefined;
});
const title = "Modrinth Servers"; const title = "Modrinth Servers";
const description = const description =
@@ -799,6 +816,8 @@ async function fetchPaymentData() {
} }
} }
const selectedProjectId = ref();
const route = useRoute(); const route = useRoute();
const isAtCapacity = computed( const isAtCapacity = computed(
() => isSmallAtCapacity.value && isMediumAtCapacity.value && isLargeAtCapacity.value, () => isSmallAtCapacity.value && isMediumAtCapacity.value && isLargeAtCapacity.value,
@@ -817,7 +836,12 @@ const scrollToFaq = () => {
} }
}; };
onMounted(scrollToFaq); onMounted(() => {
scrollToFaq();
if (route.query?.project) {
selectedProjectId.value = route.query?.project;
}
});
watch(() => route.hash, scrollToFaq); watch(() => route.hash, scrollToFaq);
@@ -876,9 +900,9 @@ const selectProduct = async (product) => {
await nextTick(); await nextTick();
if (product === "custom") { if (product === "custom") {
purchaseModal.value?.show(billingPeriod.value); purchaseModal.value?.show(billingPeriod.value, undefined, selectedProjectId.value);
} else { } else {
purchaseModal.value?.show(billingPeriod.value, selectedProduct.value); purchaseModal.value?.show(billingPeriod.value, selectedProduct.value, selectedProjectId.value);
} }
}; };
+193 -137
View File
@@ -18,48 +18,25 @@
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'" v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Server upgrading"
<div class="flex flex-col items-center gap-4"> description="Your server's hardware is currently being upgraded and will be back online shortly!"
<div class="grid place-content-center rounded-full bg-bg-blue p-4"> :icon="TransferIcon"
<TransferIcon class="size-12 text-blue" /> icon-color="blue"
</div> :action="generalErrorAction"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server upgrading</h1> />
</div>
<p class="text-lg text-secondary">
Your server's hardware is currently being upgraded and will be back online shortly!
</p>
</div>
</div>
</div> </div>
<div <div
v-else-if="serverData?.status === 'suspended'" v-else-if="serverData?.status === 'suspended'"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Server suspended"
<div class="flex flex-col items-center gap-4"> :description="suspendedDescription"
<div class="grid place-content-center rounded-full bg-bg-orange p-4"> :icon="LockIcon"
<LockIcon class="size-12 text-orange" /> icon-color="orange"
</div> :action="suspendedAction"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server suspended</h1> />
</div>
<p class="text-lg text-secondary">
{{
serverData.suspension_reason === "cancelled"
? "Your subscription has been cancelled."
: serverData.suspension_reason
? `Your server has been suspended: ${serverData.suspension_reason}`
: "Your server has been suspended."
}}
<br />
Contact Modrinth Support if you believe this is an error.
</p>
</div>
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
<button class="mt-6 !w-full">Go to billing settings</button>
</ButtonStyled>
</div>
</div> </div>
<div <div
v-else-if=" v-else-if="
@@ -68,110 +45,69 @@
" "
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="An error occured."
<div class="flex flex-col items-center gap-4"> description="Please contact Modrinth Support."
<div class="grid place-content-center rounded-full bg-bg-orange p-4"> :icon="TransferIcon"
<TransferIcon class="size-12 text-orange" /> icon-color="orange"
</div> :error-details="generalErrorDetails"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server not found</h1> :action="generalErrorAction"
</div> />
<p class="text-lg text-secondary">
You don't have permission to view this server or it no longer exists. If you believe this
is an error, please contact Modrinth Support.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
<button class="mt-6 !w-full">Go back to all servers</button>
</ButtonStyled>
</div>
</div> </div>
<div <div
v-else-if="server.moduleErrors?.general?.error.statusCode === 503" v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Server Node Unavailable"
<div class="flex flex-col items-center gap-4"> :icon="PanelErrorIcon"
<div class="grid place-content-center rounded-full bg-bg-red p-4"> icon-color="red"
<UiServersIconsPanelErrorIcon class="size-12 text-red" /> :action="nodeUnavailableAction"
</div> :error-details="nodeUnavailableDetails"
<h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1> >
<template #description>
<div class="text-md space-y-4">
<p class="leading-[170%] text-secondary">
Your server's node, where your Modrinth Server is physically hosted, is experiencing
issues. We are working with our datacenter to resolve the issue as quickly as possible.
</p>
<p class="leading-[170%] text-secondary">
Your data is safe and will not be lost, and your server will be back online as soon as
the issue is resolved.
</p>
<p class="leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
bubble in the bottom right corner and we'll be happy to help.
</p>
</div> </div>
<p class="m-0 mb-4 leading-[170%] text-secondary"> </template>
Your server's node, where your Modrinth Server is physically hosted, is experiencing </ErrorInformationCard>
issues. We are working with our datacenter to resolve the issue as quickly as possible.
</p>
<p class="m-0 mb-4 leading-[170%] text-secondary">
Your data is safe and will not be lost, and your server will be back online as soon as the
issue is resolved.
</p>
<p class="m-0 mb-4 leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
bubble in the bottom right corner and we'll be happy to help.
</p>
<div class="flex flex-col gap-2">
<UiCopyCode :text="'Server ID: ' + server.serverId" />
<UiCopyCode :text="'Node: ' + server.general?.datacenter" />
</div>
</div>
<ButtonStyled
size="large"
color="standard"
@click="
() =>
navigateTo('https://discord.modrinth.com', {
external: true,
})
"
>
<button class="mt-6 !w-full">Join Modrinth Discord</button>
</ButtonStyled>
<ButtonStyled
:disabled="formattedTime !== '00'"
size="large"
color="standard"
@click="() => reloadNuxtApp()"
>
<button class="mt-3 !w-full">Reload</button>
</ButtonStyled>
</div>
</div> </div>
<div <div
v-else-if="server.moduleErrors?.general?.error" v-else-if="server.moduleErrors?.general?.error"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Connection lost"
<div class="flex flex-col items-center gap-4"> description=""
<div class="grid place-content-center rounded-full bg-bg-orange p-4"> :icon="TransferIcon"
<TransferIcon class="size-12 text-orange" /> icon-color="orange"
</div> :action="connectionLostAction"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Connection lost</h1> >
<template #description>
<div class="space-y-4">
<div class="text-center text-secondary"> <div class="text-center text-secondary">
{{ {{
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...` formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
}} }}
</div> </div>
<p class="text-lg text-secondary">
Something went wrong, and we couldn't connect to your server. This is likely due to a
temporary network issue. You'll be reconnected automatically.
</p>
</div> </div>
<p class="text-lg text-secondary"> </template>
Something went wrong, and we couldn't connect to your server. This is likely due to a </ErrorInformationCard>
temporary network issue. You'll be reconnected automatically.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
<ButtonStyled
:disabled="formattedTime !== '00'"
size="large"
color="brand"
@click="() => reloadNuxtApp()"
>
<button class="mt-6 !w-full">Reload</button>
</ButtonStyled>
</div>
</div> </div>
<!-- SERVER START --> <!-- SERVER START -->
<div <div
@@ -432,7 +368,7 @@ import {
LockIcon, LockIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { ButtonStyled, ServerNotice } from "@modrinth/ui"; import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
import { Intercom, shutdown } from "@intercom/messenger-js-sdk"; import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
import type { MessageDescriptor } from "@vintl/vintl"; import type { MessageDescriptor } from "@vintl/vintl";
import type { import type {
@@ -448,6 +384,7 @@ import { useModrinthServersConsole } from "~/store/console.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts"; import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts"; import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue"; import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
import PanelErrorIcon from "~/components/ui/servers/icons/PanelErrorIcon.vue";
const app = useNuxtApp() as unknown as { $notify: any }; const app = useNuxtApp() as unknown as { $notify: any };
@@ -760,7 +697,7 @@ const startUptimeUpdates = () => {
const stopUptimeUpdates = () => { const stopUptimeUpdates = () => {
if (uptimeIntervalId) { if (uptimeIntervalId) {
clearInterval(uptimeIntervalId); clearInterval(uptimeIntervalId);
intervalId = null; pollingIntervalId = null;
} }
}; };
@@ -1055,7 +992,7 @@ const notifyError = (title: string, text: string) => {
}); });
}; };
let intervalId: ReturnType<typeof setInterval> | null = null; let pollingIntervalId: ReturnType<typeof setInterval> | null = null;
const countdown = ref(15); const countdown = ref(15);
const formattedTime = computed(() => { const formattedTime = computed(() => {
@@ -1099,23 +1036,142 @@ const backupInProgress = computed(() => {
}); });
const stopPolling = () => { const stopPolling = () => {
if (intervalId) { if (pollingIntervalId) {
clearInterval(intervalId); clearTimeout(pollingIntervalId);
intervalId = null; pollingIntervalId = null;
} }
}; };
const startPolling = () => { const startPolling = () => {
countdown.value = 15; stopPolling();
intervalId = setInterval(() => {
if (countdown.value <= 0) { let retryCount = 0;
reloadNuxtApp(); const maxRetries = 10;
} else {
countdown.value--; const poll = async () => {
try {
await server.refresh(["general", "ws"]);
if (!server.moduleErrors?.general?.error) {
stopPolling();
connectWebSocket();
return;
}
retryCount++;
if (retryCount >= maxRetries) {
console.error("Max retries reached, stopping polling");
stopPolling();
return;
}
// Exponential backoff: 3s, 6s, 12s, 24s, etc.
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
} catch (error) {
console.error("Polling failed:", error);
retryCount++;
if (retryCount < maxRetries) {
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
}
} }
}, 1000); };
poll();
}; };
const nodeUnavailableDetails = computed(() => [
{
label: "Server ID",
value: server.serverId,
type: "inline" as const,
},
{
label: "Node",
value: server.general?.datacenter ?? "Unknown! Please contact support!",
type: "inline" as const,
},
]);
const suspendedDescription = computed(() => {
if (serverData.value?.suspension_reason === "cancelled") {
return "Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error.";
}
if (serverData.value?.suspension_reason) {
return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`;
}
return "Your server has been suspended.\nContact Modrinth Support if you believe this is an error.";
});
const generalErrorDetails = computed(() => [
{
label: "Server ID",
value: server.serverId,
type: "inline" as const,
},
{
label: "Timestamp",
value: String(server.moduleErrors?.general?.timestamp),
type: "inline" as const,
},
{
label: "Error Name",
value: server.moduleErrors?.general?.error.name,
type: "inline" as const,
},
{
label: "Error Message",
value: server.moduleErrors?.general?.error.message,
type: "block" as const,
},
...(server.moduleErrors?.general?.error.originalError
? [
{
label: "Original Error",
value: String(server.moduleErrors.general.error.originalError),
type: "hidden" as const,
},
]
: []),
...(server.moduleErrors?.general?.error.stack
? [
{
label: "Stack Trace",
value: server.moduleErrors.general.error.stack,
type: "hidden" as const,
},
]
: []),
]);
const suspendedAction = computed(() => ({
label: "Go to billing settings",
onClick: () => router.push("/settings/billing"),
color: "brand" as const,
}));
const generalErrorAction = computed(() => ({
label: "Go back to all servers",
onClick: () => router.push("/servers/manage"),
color: "brand" as const,
}));
const nodeUnavailableAction = computed(() => ({
label: "Join Modrinth Discord",
onClick: () => navigateTo("https://discord.modrinth.com", { external: true }),
color: "standard" as const,
}));
const connectionLostAction = computed(() => ({
label: "Reload",
onClick: () => reloadNuxtApp(),
color: "brand" as const,
disabled: formattedTime.value !== "00",
}));
const copyServerDebugInfo = () => { const copyServerDebugInfo = () => {
const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`; const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`;
navigator.clipboard.writeText(debugInfo); navigator.clipboard.writeText(debugInfo);
@@ -155,26 +155,18 @@ const saveGeneral = async () => {
if (serverSubdomain.value !== data.value?.net?.domain) { if (serverSubdomain.value !== data.value?.net?.domain) {
try { try {
// type shit backend makes me do // type shit backend makes me do
const response = await props.server.network?.checkSubdomainAvailability( const available = await props.server.network?.checkSubdomainAvailability(
serverSubdomain.value, serverSubdomain.value,
); );
if (response === undefined) {
throw new Error("Failed to check subdomain availability");
}
if (typeof response === "object" && response !== null && "available" in response) { if (!available) {
const typedResponse = response as { available: boolean }; addNotification({
if (!typedResponse.available) { group: "serverOptions",
addNotification({ type: "error",
group: "serverOptions", title: "Subdomain not available",
type: "error", text: "The subdomain you entered is already in use.",
title: "Subdomain not available", });
text: "The subdomain you entered is already in use.", return;
});
return;
}
} else {
throw new Error("Invalid response format from availability check");
} }
await props.server.network?.changeSubdomain(serverSubdomain.value); await props.server.network?.changeSubdomain(serverSubdomain.value);
@@ -330,8 +330,7 @@
<ButtonStyled <ButtonStyled
v-if=" v-if="
getPyroCharge(subscription) && getPyroCharge(subscription) &&
getPyroCharge(subscription).status !== 'cancelled' && getPyroCharge(subscription).status !== 'cancelled'
getPyroCharge(subscription).status !== 'failed'
" "
> >
<button @click="showCancellationSurvey(subscription)"> <button @click="showCancellationSurvey(subscription)">
+1 -1
View File
@@ -200,9 +200,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { CodeIcon, RadioButtonCheckedIcon, RadioButtonIcon } from "@modrinth/assets"; import { CodeIcon, RadioButtonCheckedIcon, RadioButtonIcon } from "@modrinth/assets";
import { Button, ThemeSelector } from "@modrinth/ui"; import { Button, ThemeSelector } from "@modrinth/ui";
import { formatProjectType } from "@modrinth/utils";
import MessageBanner from "~/components/ui/MessageBanner.vue"; import MessageBanner from "~/components/ui/MessageBanner.vue";
import type { DisplayLocation } from "~/plugins/cosmetics"; import type { DisplayLocation } from "~/plugins/cosmetics";
import { formatProjectType } from "~/plugins/shorthands.js";
import { isDarkTheme, type Theme } from "~/plugins/theme/index.ts"; import { isDarkTheme, type Theme } from "~/plugins/theme/index.ts";
useHead({ useHead({
+1 -1
View File
@@ -205,6 +205,7 @@
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets"; import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { import {
Checkbox, Checkbox,
CopyCode,
ConfirmModal, ConfirmModal,
commonSettingsMessages, commonSettingsMessages,
commonMessages, commonMessages,
@@ -219,7 +220,6 @@ import {
getScopeValue, getScopeValue,
} from "~/composables/auth/scopes.ts"; } from "~/composables/auth/scopes.ts";
import CopyCode from "~/components/ui/CopyCode.vue";
import Modal from "~/components/ui/Modal.vue"; import Modal from "~/components/ui/Modal.vue";
const { formatMessage } = useVIntl(); const { formatMessage } = useVIntl();
+1 -1
View File
@@ -355,6 +355,7 @@ import {
GlobeIcon, GlobeIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { import {
Avatar,
OverflowMenu, OverflowMenu,
ButtonStyled, ButtonStyled,
ContentPageHeader, ContentPageHeader,
@@ -377,7 +378,6 @@ import BetaTesterBadge from "~/assets/images/badges/beta-tester.svg?component";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component"; import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import Avatar from "~/components/ui/Avatar.vue";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue"; import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue"; import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
@@ -10,6 +10,10 @@ export default defineNuxtPlugin((nuxtApp) => {
instantMove: true, instantMove: true,
distance: 8, distance: 8,
}, },
"dismissable-prompt": {
$extend: "dropdown",
placement: "bottom-start",
},
}, },
}); });
}); });
-84
View File
@@ -13,11 +13,6 @@ export default defineNuxtPlugin((nuxtApp) => {
return cosmeticsStore.externalLinksNewTab ? "_blank" : ""; return cosmeticsStore.externalLinksNewTab ? "_blank" : "";
}); });
nuxtApp.provide("formatBytes", formatBytes);
nuxtApp.provide("formatWallet", formatWallet);
nuxtApp.provide("formatProjectType", formatProjectType);
nuxtApp.provide("formatCategory", formatCategory);
nuxtApp.provide("formatCategoryHeader", formatCategoryHeader);
/* /*
Only use on the complete list of versions for a project, partial lists will generate Only use on the complete list of versions for a project, partial lists will generate
@@ -156,89 +151,10 @@ export const formatMoney = (number, abbreviate = false) => {
} }
}; };
export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) {
return "0 Bytes";
}
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KiB", "MiB", "GiB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
};
export const capitalizeString = (name) => { export const capitalizeString = (name) => {
return name ? name.charAt(0).toUpperCase() + name.slice(1) : name; return name ? name.charAt(0).toUpperCase() + name.slice(1) : name;
}; };
export const formatWallet = (name) => {
if (name === "paypal") {
return "PayPal";
}
return capitalizeString(name);
};
export const formatProjectType = (name) => {
if (name === "resourcepack") {
return "Resource Pack";
} else if (name === "datapack") {
return "Data Pack";
}
return capitalizeString(name);
};
export const formatCategory = (name) => {
if (name === "modloader") {
return "Risugami's ModLoader";
} else if (name === "bungeecord") {
return "BungeeCord";
} else if (name === "liteloader") {
return "LiteLoader";
} else if (name === "neoforge") {
return "NeoForge";
} else if (name === "game-mechanics") {
return "Game Mechanics";
} else if (name === "worldgen") {
return "World Generation";
} else if (name === "core-shaders") {
return "Core Shaders";
} else if (name === "gui") {
return "GUI";
} else if (name === "8x-") {
return "8x or lower";
} else if (name === "512x+") {
return "512x or higher";
} else if (name === "kitchen-sink") {
return "Kitchen Sink";
} else if (name === "path-tracing") {
return "Path Tracing";
} else if (name === "pbr") {
return "PBR";
} else if (name === "datapack") {
return "Data Pack";
} else if (name === "colored-lighting") {
return "Colored Lighting";
} else if (name === "optifine") {
return "OptiFine";
} else if (name === "mrpack") {
return "Modpack";
} else if (name === "minecraft") {
return "Resource Pack";
} else if (name === "vanilla") {
return "Vanilla Shader";
}
return capitalizeString(name);
};
export const formatCategoryHeader = (name) => {
return capitalizeString(name);
};
export const formatVersions = (tag, versionArray) => { export const formatVersions = (tag, versionArray) => {
const allVersions = tag.value.gameVersions.slice().reverse(); const allVersions = tag.value.gameVersions.slice().reverse();
const allReleases = allVersions.filter((x) => x.version_type === "release"); const allReleases = allVersions.filter((x) => x.version_type === "release");
File diff suppressed because it is too large. Load diff
@@ -1,84 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modrinth App Ad</title>
<script type="module" src="//js.rev.iq" data-domain="modrinth.com"></script>
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
cursor: pointer;
}
.ads-container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
#plus-link {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
#modrinth-rail-1 {
border-radius: 1rem;
position: absolute;
left: 0;
top: 0;
z-index: 2;
}
</style>
</head>
<body>
<div class="ads-container">
<div id="plus-link"></div>
<div id="modrinth-rail-1" data-ad="app-rail" />
</div>
<script>
// window.addEventListener(
// "message",
// (event) => {
// if (event.data.modrinthAdClick && window.__TAURI_INTERNALS__) {
// window.__TAURI_INTERNALS__.invoke("plugin:ads|record_ads_click", {});
// }
//
// if (event.data.modrinthOpenUrl && window.__TAURI_INTERNALS__) {
// window.__TAURI_INTERNALS__.invoke("plugin:ads|open_link", {
// path: event.data.modrinthOpenUrl,
// origin: event.origin,
// });
// }
// },
// false,
// );
window.addEventListener("mousewheel", (event) => {
if (window.__TAURI_INTERNALS__) {
window.__TAURI_INTERNALS__.invoke("plugin:ads|scroll_ads_window", {
scroll: event.deltaY,
});
}
});
document.addEventListener("contextmenu", (event) => event.preventDefault());
const plusLink = document.getElementById("plus-link");
plusLink.addEventListener("click", function () {
window.__TAURI_INTERNALS__.invoke("plugin:ads|record_ads_click", {});
window.__TAURI_INTERNALS__.invoke("plugin:ads|open_link", {
path: "https://modrinth.com/plus",
origin: "https://modrinth.com",
});
});
</script>
</body>
</html>
+14 -10
View File
@@ -3,7 +3,8 @@ RUST_LOG=info,sqlx::query=warn
SENTRY_DSN=none SENTRY_DSN=none
SITE_URL=http://localhost:3000 SITE_URL=http://localhost:3000
CDN_URL=https://staging-cdn.modrinth.com # This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH
CDN_URL=file:///tmp/modrinth
LABRINTH_ADMIN_KEY=feedbeef LABRINTH_ADMIN_KEY=feedbeef
RATE_LIMIT_IGNORE_KEY=feedbeef RATE_LIMIT_IGNORE_KEY=feedbeef
@@ -25,18 +26,21 @@ PUBLIC_DISCORD_WEBHOOK=
CLOUDFLARE_INTEGRATION=false CLOUDFLARE_INTEGRATION=false
STORAGE_BACKEND=local STORAGE_BACKEND=local
MOCK_FILE_PATH=/tmp/modrinth MOCK_FILE_PATH=/tmp/modrinth
BACKBLAZE_KEY_ID=none S3_PUBLIC_BUCKET_NAME=none
BACKBLAZE_KEY=none S3_PUBLIC_USES_PATH_STYLE_BUCKET=false
BACKBLAZE_BUCKET_ID=none S3_PUBLIC_REGION=none
S3_PUBLIC_URL=none
S3_PUBLIC_ACCESS_TOKEN=none
S3_PUBLIC_SECRET=none
S3_ACCESS_TOKEN=none S3_PRIVATE_BUCKET_NAME=none
S3_SECRET=none S3_PRIVATE_USES_PATH_STYLE_BUCKET=false
S3_URL=none S3_PRIVATE_REGION=none
S3_REGION=none S3_PRIVATE_URL=none
S3_BUCKET_NAME=none S3_PRIVATE_ACCESS_TOKEN=none
S3_PRIVATE_SECRET=none
# 1 hour # 1 hour
LOCAL_INDEX_INTERVAL=3600 LOCAL_INDEX_INTERVAL=3600
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET moderation_message = NULL, moderation_message_body = NULL, queued = NOW()\n WHERE (id = $1)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "010cafcafb6adc25b00e3c81d844736b0245e752a90334c58209d8a02536c800"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO shared_instance_users (user_id, shared_instance_id, permissions)\n VALUES ($1, $2, $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "09ebec1a568edf1959f20b33d8ba2b8edb55d93ada8f2243448865163f555d8d"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET webhook_sent = TRUE\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "124fbf0544ea6989d6dc5e840405dbc76d7385276a38ad79d9093c53c73bbde2"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM mods_links\n WHERE joining_mod_id = $1 AND joining_platform_id IN (\n SELECT id FROM link_platforms WHERE name = ANY($2)\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"TextArray"
]
},
"nullable": []
},
"hash": "186d0e933ece20163915926293a01754ff571de4f06e521bb4f7c0207268e03b"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET license = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "19dc22c4d6d14222f8e8bace74c2961761c53b7375460ade15af921754d5d7da"
}
@@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, title, owner_id, public, current_version_id\n FROM shared_instances\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "owner_id",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "public",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "current_version_id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
true
]
},
"hash": "1ebe19b7b4f10039065967a0b1ca4bb38acc54e4ea5de020fffef7457000fa6e"
}
@@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, shared_instance_id, size, sha512, created\n FROM shared_instance_versions\n WHERE shared_instance_id = $1\n ORDER BY created DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "shared_instance_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "size",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "sha512",
"type_info": "Bytea"
},
{
"ordinal": 4,
"name": "created",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "265c4d6f33714c8a5cf3137c429e2b57e917e9507942d65f40c1b733209cabf0"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "299b8ea6e7a0048fa389cc4432715dc2a09e227d2f08e91167a43372a7ac6e35"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET side_types_migration_review_status = $1\n WHERE id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "374c234b92b838b0bd65de100a9008b0fb78c79976fd858e0599e1ccb7f08b82"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = TRUE\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "3fcfed18cbfb37866e0fa57a4e95efb326864f8219941d1b696add39ed333ad1"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = TRUE\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "40f7c5bec98fe3503d6bd6db2eae5a4edb8d5d6efda9b9dc124f344ae5c60e08"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM shared_instance_versions\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "47130ef29ce5914528e5424fe516a9158a3ea08f8720f6df5b4902cd8094d3bb"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE shared_instances SET current_version_id = $1 WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "47ec9f179f1c52213bd32b37621ab13ae43d180b8c86cb2a6fab0253dd4eba55"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET description = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "4e9f9eafbfd705dfc94571018cb747245a98ea61bad3fae4b3ce284229d99955"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET slug = LOWER($1)\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": []
},
"hash": "4fb5bd341369b4beb6b4a88de296b608ea5441a96db9f7360fbdccceb4628202"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET summary = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "595f4e7432d5b41002988c6cc6b0b1f09273ad02c319e6631c74d80a9b278328"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mods_links (joining_mod_id, joining_platform_id, url)\n VALUES ($1, $2, $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "6366891bb34a14278f1ae857b8d6f68dff44badae9ae5c5aceba3c32e8d00356"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET requested_status = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "66b06ddcd0a4cf01e716331befa393a12631fe6752a7d078bda06b24d50daae2"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE shared_instances\n SET public = $1\n WHERE id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Int8"
]
},
"nullable": []
},
"hash": "6b166d129b0ee028898620054a58fa4c3641eb2221e522bf50abad4f5e977599"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO shared_instances (id, title, owner_id, current_version_id)\n VALUES ($1, $2, $3, $4)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Varchar",
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "6f72c853e139f23322fe6f1f02e4e07e5ae80b5dfca6dc041a03c0c7a30a5cf1"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET name = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "70be97b02e402de0490ade5866c47232f9c341add2f3838cc3ae1a07a310d561"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE shared_instances\n SET owner_id = $1\n WHERE owner_id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "72ae0e8debd06067894a2f7bea279446dd964da4efa49c5464cebde57860f741"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET moderation_message_body = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "79040825457845cc078be7b3293804d6fb2e05ffce07e7b4248d8705d6fc6e61"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET monetization_status = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "7916fe4f04067324ae05598ec9dc6f97f18baf9eda30c64f32677158ada87478"
}
@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,\n m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ", "query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,\n m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n m.side_types_migration_review_status side_types_migration_review_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -125,11 +125,16 @@
}, },
{ {
"ordinal": 24, "ordinal": 24,
"name": "side_types_migration_review_status",
"type_info": "Varchar"
},
{
"ordinal": 25,
"name": "categories", "name": "categories",
"type_info": "VarcharArray" "type_info": "VarcharArray"
}, },
{ {
"ordinal": 25, "ordinal": 26,
"name": "additional_categories", "name": "additional_categories",
"type_info": "VarcharArray" "type_info": "VarcharArray"
} }
@@ -165,9 +170,10 @@
true, true,
false, false,
false, false,
false,
null, null,
null null
] ]
}, },
"hash": "5f75c0c48083de27f853ee877aac070567fd2ed2be4a9a038821b790dd7cb763" "hash": "7a6d6a91e6bd27f7be34b8cc7955a66c4175ebd1c55e437f187f61efca681c62"
} }
@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)\n ", "query": "SELECT EXISTS(SELECT 1 FROM shared_instance_versions WHERE id=$1)",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -18,5 +18,5 @@
null null
] ]
}, },
"hash": "f34bbe639ad21801258dc8beaab9877229a451761be07f85a1dd04d027832329" "hash": "7c445073f61e30723416a9690aa9d227d95f2a8f2eb9852833e14c723903988b"
} }
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET slug = LOWER($1)\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": []
},
"hash": "7e403d399ddd3279c4c65db7b9ea850cdd9fef3df1b3f7d5f62e079b4522f2ca"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET approved = NOW()\n WHERE id = $1 AND approved IS NULL\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "92d805d2e13cfc0f2220f15b0a35ff71e654e5e6b386766e6c6047cf3861b26e"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET license_url = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "9482a3419337911ac6a10eeaf065e29589ee1b707729344e81d183c713aa0d28"
}
@@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n -- See https://github.com/launchbadge/sqlx/issues/1266 for why we need all the \"as\"\n SELECT\n id as \"id!\",\n title as \"title!\",\n public as \"public!\",\n owner_id as \"owner_id!\",\n current_version_id\n FROM shared_instances\n WHERE owner_id = $1\n UNION\n SELECT\n id as \"id!\",\n title as \"title!\",\n public as \"public!\",\n owner_id as \"owner_id!\",\n current_version_id\n FROM shared_instances\n JOIN shared_instance_users ON id = shared_instance_id\n WHERE user_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "title!",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "public!",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "owner_id!",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "current_version_id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null,
null,
null,
null,
null
]
},
"hash": "9c6e18cb19251e54b3b96446ab88d84842152b82c9a0032d1db587d7099b8550"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE shared_instances\n SET title = $1\n WHERE id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "9ccaf8ea52b1b6f0880d34cdb4a9405e28c265bef6121b457c4f39cacf00683f"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1 AND is_additional = FALSE\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "a0148ff25855202e7bb220b6a2bc9220a95e309fb0dae41d9a05afa86e6b33af"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET approved = NOW()\n WHERE id = $1 AND approved IS NULL\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "a11d613479d09dff5fcdc45ab7a0341fb1b4738f0ede71572d939ef0984bd65f"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET license = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "a5ae1fe0ca4ca8432736398fed25687173b2fbde3405340a5579c5ef68cb5218"
}
Loaded 100 of 230 files, more files were not shown because too many files have changed in this diff. Show more