Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f07bc86711 | ||
|
|
23f0c1dbf7 | ||
|
|
8abbc021ea | ||
|
|
ed8ff79809 | ||
|
|
f210de563d |
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||
[target.'cfg(windows)']
|
||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
+2
-2
@@ -14,5 +14,5 @@ max_line_length = 100
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{rs,java,kts}]
|
||||
indent_size = 4
|
||||
[*.rs]
|
||||
indent_size = 4
|
||||
@@ -9,18 +9,14 @@ on:
|
||||
- .github/workflows/theseus-release.yml
|
||||
- 'apps/app/**'
|
||||
- 'apps/app-frontend/**'
|
||||
- 'apps/labrinth/src/common/**'
|
||||
- 'apps/labrinth/Cargo.toml'
|
||||
- 'packages/app-lib/**'
|
||||
- 'packages/app-macros/**'
|
||||
- 'packages/assets/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/utils/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign-windows-binaries:
|
||||
description: Sign Windows binaries
|
||||
type: boolean
|
||||
default: true
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -107,21 +103,11 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Install 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
|
||||
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)
|
||||
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')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -135,30 +121,15 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: build app (Linux)
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
- name: build app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
|
||||
id: build_os
|
||||
if: "!startsWith(matrix.platform, 'macos')"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 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 }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
Generated
+1
-1
@@ -17,4 +17,4 @@
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
Generated
+1
-1
@@ -5,4 +5,4 @@
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
Generated
+203
-410
File diff suppressed because it is too large.
Load diff
+17
-23
@@ -25,7 +25,7 @@ actix-ws = "0.3.0"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async_zip = "0.0.17"
|
||||
async-compression = { version = "0.4.25", default-features = false }
|
||||
async-compression = { version = "0.4.24", default-features = false }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
@@ -37,7 +37,6 @@ async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
||||
async-walkdir = "2.1.0"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.1"
|
||||
bytemuck = "1.23.0"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
@@ -48,7 +47,6 @@ color-thief = "0.2.2"
|
||||
console-subscriber = "0.4.1"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
dashmap = "6.1.0"
|
||||
data-url = "0.3.1"
|
||||
deadpool-redis = "0.21.1"
|
||||
dirs = "6.0.0"
|
||||
discord-rich-presence = "0.2.5"
|
||||
@@ -62,7 +60,6 @@ flate2 = "1.1.2"
|
||||
fs4 = { version = "0.13.1", default-features = false }
|
||||
futures = { version = "0.3.31", default-features = false }
|
||||
futures-util = "0.3.31"
|
||||
hashlink = "0.10.0"
|
||||
heck = "0.5.0"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25.2"
|
||||
@@ -93,7 +90,6 @@ notify = { version = "8.0.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
||||
p256 = "0.13.2"
|
||||
paste = "1.0.15"
|
||||
png = "0.17.16"
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.37.5"
|
||||
@@ -101,9 +97,8 @@ rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.20", default-features = false }
|
||||
rgb = "0.8.50"
|
||||
rust_decimal = { version = "1.37.2", features = [
|
||||
reqwest = { version = "0.12.19", default-features = false }
|
||||
rust_decimal = { version = "1.37.1", features = [
|
||||
"serde-with-float",
|
||||
"serde-with-str",
|
||||
] }
|
||||
@@ -114,7 +109,7 @@ rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||
"tokio-rustls-tls",
|
||||
] }
|
||||
rusty-money = "0.4.1"
|
||||
sentry = { version = "0.41.0", default-features = false, features = [
|
||||
sentry = { version = "0.38.1", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
@@ -122,13 +117,13 @@ sentry = { version = "0.41.0", default-features = false, features = [
|
||||
"reqwest",
|
||||
"rustls",
|
||||
] }
|
||||
sentry-actix = "0.41.0"
|
||||
sentry-actix = "0.38.1"
|
||||
serde = "1.0.219"
|
||||
serde_bytes = "0.11.17"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_with = "3.13.0"
|
||||
serde_with = "3.12.0"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
@@ -137,19 +132,18 @@ spdx = "0.10.8"
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
sysinfo = { version = "0.35.2", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tauri = "2.6.1"
|
||||
tauri-build = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.0"
|
||||
tauri-plugin-dialog = "2.3.0"
|
||||
tauri-plugin-http = "2.5.0"
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
tauri-plugin-os = "2.3.0"
|
||||
tauri-plugin-single-instance = "2.3.0"
|
||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||
tauri = "2.5.1"
|
||||
tauri-build = "2.2.0"
|
||||
tauri-plugin-deep-link = "2.3.0"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-opener = "2.2.7"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-single-instance = "2.2.4"
|
||||
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"zip",
|
||||
] }
|
||||
tauri-plugin-window-state = "2.3.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
tempfile = "3.20.0"
|
||||
theseus = { path = "packages/app-lib" }
|
||||
thiserror = "2.0.12"
|
||||
@@ -172,7 +166,7 @@ whoami = "1.6.0"
|
||||
winreg = "0.55.0"
|
||||
woothee = "0.13.0"
|
||||
yaserde = "0.12.0"
|
||||
zip = { version = "4.2.0", default-features = false, features = [
|
||||
zip = { version = "4.0.0", default-features = false, features = [
|
||||
"bzip2",
|
||||
"deflate",
|
||||
"deflate64",
|
||||
@@ -219,7 +213,7 @@ wildcard_dependencies = "warn"
|
||||
warnings = "deny"
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "21db186" }
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
||||
|
||||
# Optimize for speed and reduce size on release builds
|
||||
[profile.release]
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
**/dist
|
||||
*.gltf
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.10.0",
|
||||
"version": "0.9.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -20,20 +20,16 @@
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
"ofetch": "^1.3.4",
|
||||
"pinia": "^2.1.7",
|
||||
"posthog-js": "^1.158.2",
|
||||
"three": "^0.172.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-multiselect": "3.0.0",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch, provide } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChangeSkinIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
HomeIcon,
|
||||
@@ -26,7 +25,7 @@ import {
|
||||
ButtonStyled,
|
||||
Notifications,
|
||||
OverflowMenu,
|
||||
NewsArticleCard,
|
||||
useRelativeTime,
|
||||
} from '@modrinth/ui'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
@@ -63,13 +62,14 @@ import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -177,7 +177,6 @@ async function setupApp() {
|
||||
'criticalAnnouncements',
|
||||
true,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
if (res && res.header && res.body) {
|
||||
criticalErrorMessage.value = res
|
||||
@@ -189,33 +188,15 @@ async function setupApp() {
|
||||
)
|
||||
})
|
||||
|
||||
useFetch(`https://modrinth.com/news/feed/articles.json`, 'news', true)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
if (res && res.articles) {
|
||||
// Format expected by NewsArticleCard component.
|
||||
news.value = res.articles.map((article) => ({
|
||||
...article,
|
||||
path: article.link,
|
||||
thumbnail: article.thumbnail,
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
date: article.date,
|
||||
}))
|
||||
}
|
||||
})
|
||||
useFetch(`https://modrinth.com/blog/news.json`, 'news', true).then((res) => {
|
||||
if (res && res.articles) {
|
||||
news.value = res.articles
|
||||
}
|
||||
})
|
||||
|
||||
get_opening_command().then(handleCommand)
|
||||
checkUpdates()
|
||||
fetchCredentials()
|
||||
|
||||
try {
|
||||
const skins = (await get_available_skins()) ?? []
|
||||
const capes = (await get_available_capes()) ?? []
|
||||
generateSkinPreviews(skins, capes)
|
||||
} catch (error) {
|
||||
console.warn('Failed to generate skin previews in app setup.', error)
|
||||
}
|
||||
}
|
||||
|
||||
const stateFailed = ref(false)
|
||||
@@ -323,7 +304,6 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const accounts = ref(null)
|
||||
provide('accountsCard', accounts)
|
||||
|
||||
command_listener(handleCommand)
|
||||
async function handleCommand(e) {
|
||||
@@ -419,9 +399,6 @@ function handleAuxClick(e) {
|
||||
>
|
||||
<CompassIcon />
|
||||
</NavButton>
|
||||
<NavButton v-tooltip.right="'Skins (Beta)'" to="/skins">
|
||||
<ChangeSkinIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Library'"
|
||||
to="/library"
|
||||
@@ -602,15 +579,34 @@ function handleAuxClick(e) {
|
||||
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div v-if="news && news.length > 0" class="pt-4 flex flex-col items-center">
|
||||
<h3 class="px-4 text-lg m-0 text-left w-full">News</h3>
|
||||
<div class="px-4 pt-2 space-y-4 flex flex-col items-center w-full">
|
||||
<NewsArticleCard
|
||||
v-for="(item, index) in news"
|
||||
:key="`news-${index}`"
|
||||
:article="item"
|
||||
<div v-if="news && news.length > 0" class="pt-4 flex flex-col">
|
||||
<h3 class="px-4 text-lg m-0">News</h3>
|
||||
<template v-for="(item, index) in news" :key="`news-${index}`">
|
||||
<a
|
||||
:class="`flex flex-col outline-offset-[-4px] hover:bg-[--brand-gradient-border] focus:bg-[--brand-gradient-border] px-4 transition-colors ${index === 0 ? 'pt-2 pb-4' : 'py-4'}`"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
rel="external"
|
||||
>
|
||||
<img
|
||||
:src="item.thumbnail"
|
||||
alt="News thumbnail"
|
||||
aria-hidden="true"
|
||||
class="w-full aspect-[3/1] object-cover rounded-2xl border-[1px] border-solid border-[--brand-gradient-border]"
|
||||
/>
|
||||
<h4 class="mt-2 mb-0 text-sm leading-none text-contrast font-semibold">
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
|
||||
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
|
||||
{{ formatRelativeTime(dayjs(item.date).toISOString()) }}
|
||||
</p>
|
||||
</a>
|
||||
<hr
|
||||
v-if="index !== news.length - 1"
|
||||
class="h-px my-[-2px] mx-4 border-0 m-0 bg-[--brand-gradient-border]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"asset":{"version":"2.0","generator":"Blockbench 4.12.4 glTF exporter"},"scenes":[{"nodes":[1],"name":"blockbench_export"}],"scene":0,"nodes":[{"rotation":[0,0,0.19509032201612825,0.9807852804032304],"translation":[0.15625,1,0],"name":"Cape","mesh":0},{"children":[0]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AAAAPQAAAAAAAKA+AAAAPQAAAAAAAKC+AAAAPQAAgL8AAKA+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAvQAAgL8AAKC+AAAAvQAAgL8AAKA+AAAAvQAAAAAAAKC+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAvQAAgL8AAKC+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKC+AAAAPQAAgL8AAKC+AAAAvQAAgL8AAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPAAAgD0AADA+AACAPQAAgDwAAAg/AAAwPgAACD8AAEA+AACAPQAAsD4AAIA9AABAPgAACD8AALA+AAAIPwAAgDwAAAA9AACAPAAAgD0AADA+AAAAPQAAMD4AAIA9AAAwPgAAAD0AAKg+AAAAPQAAMD4AAAAAAACoPgAAAAAAAEA+AACAPQAAMD4AAIA9AABAPgAACD8AADA+AAAIPwAAAAAAAIA9AACAPAAAgD0AAAAAAAAIPwAAgDwAAAg/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUA"}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[0.03125,0,0.3125],"min":[-0.03125,-1,-0.3125],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"max":[0.34375,0.53125],"min":[0,0],"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"}],"materials":[{"pbrMetallicRoughness":{"metallicFactor":0,"roughnessFactor":1,"baseColorTexture":{"index":0}},"alphaMode":"MASK","alphaCutoff":0.05,"doubleSided":true}],"textures":[{"sampler":0,"source":0,"name":"cape.png"}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"images":[{"mimeType":"image/png","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAABCRJREFUaEPtlktoE1EUhm/sIzFpYsBgHyn2oRZt0C5ESxWFYutKNyKIWBBR6kpBd3Ur2J0FRdCiuKmIIG4qLnzgQqTUXSlVqdoXTU0lljRpYtOHI/8dznBnOm1mphMwJWeTzJ0795zznf/cex2MMXam2S/ht38siJ8V1lgd5mOBARerc3rZnmK37rwvCyk2nE6wezMRh+4EncH9B1ql+fkU64rPsaBz5bqh4T7Daxn1Kc5zVNeEePJIci0Aq71bzenY6JChwAnA0OBHQ/OtJLnWNwqAy61R1tO3k891ueRKoDKwtqbv7MGbgCnfRgHQoqG9hyX4hc/yiho+/HNqVPFtdj2jwTpQgd/RKT7/0ZUku/o4qAJw50KYXbzr4e+3BioYzc3kwGzAUCLWFw2+UBiCb3bNTDHiPQeAP3CGNmg/6VcSBpDu3hhvDQpOCwABwrQKsRIsqYAChy/EQAXwlPiZ3a3igNPkXIyTjr8o5L7PPdzGf59c+sV/faeWeIIIAHPJ8M3kc7l1K09LKghWAIgqoPZ7djPFTlxb4D6yAgBOQfntrUVWVeRk44tplXJorOVGkVIJTBCTpw9ECFYBIEkyMfmsAXh3u1qi5MlxbbGX/x1ZSCjBAAwgfPr6h49R5UVavk0FXC2wju5p07s6iqEF0PtqSlEf+bKzDRwdgaDU7AkoySJ5Oo/D6ZRq/H0yyuJ/lxkSheG/FgCNm7kL0BoEAG32sqtY1YIHd2/mGzTMVgD3y2slqjgW9xY6ma9AThAARIMiBtOpjADwTWc0bEkB+FZsSTxTW0KBsGPXx0yvrUpEeHAAQIM7wBJLcu8DwHaP/H8i6VSND6SiSjBlRW4WWUypVIBbIgzjVgB0tpdKqLReS0KVPTMTfH0ra2cEgAmAAEd+l1z52LybqwBQYAQAyZPh6gtDW4jjuC4fHx8wXSm0JDbeI95SRYHiFZlUaWVtPQiKAugl5E8ASAUYiy8vc0C474uGasPE5PHc4g0wK/f4obom6UNimol7kTZwQLANwOuqBokqDEeQf4lfvvnNxZJcBTBAGZplWQcQ3tcgwY9oWlXinRW4ugoAgNAWWe6ocn1QvgyRTUb4RZFVljlY/3hSBYCqb6cCZo8ekuATVRZPI/gQW8FWAI1VHganVgDQUajdA6y2gAgAcZFBjTBSpG0AcAqc3VVmCMDTbxGWZvIRCaMNkJ7pFMCzVQD4liCQ8kRFUlvaBkCvL+wYw2ZmNUgCgLajFhRPJlv3ADuS1VtjPQCk823S574ffN/x1dQqy8dHR5SN2Spcbaymz2mjwNYDAD4IQn3TDpVLQIAqNjwAANRrAdoI/3sAOF7Xe1khCNQGVH1bL0JGJb1R52VtD8gVYHkAuVKpbMWZV0C2yObKunkF5EqlshVnXgHZIpsr6/4DlbxcPydnT74AAAAASUVORK5CYII="}],"meshes":[{"primitives":[{"mode":4,"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"asset":{"generator":"FBX2glTF v0.13.1","version":"2.0"},"scene":0,"buffers":[{"byteLength":71248,"uri":"data:application/octet-stream;base64,AAAAAAAAAACJiAg9iYiIPc3MzD2JiAg+q6oqPs3MTD7v7m4+iYiIPpqZmT6rqqo+vLu7Ps3MzD7e3d0+7+7uPgAAAD+JiAg/ERERP5qZGT8iIiI/q6oqPzMzMz+8uzs/REREP83MTD9VVVU/3t1dP2ZmZj/v7m4/d3d3PwAAgD9ERIQ/iYiIP83MjD8REZE/VVWVP5qZmT/e3Z0/IiKiP2Zmpj+rqqo/7+6uPzMzsz93d7c/vLu7PwAAwD9ERMQ/iYjIP83MzD8REdE/VVXVP5qZ2T/e3d0/IiLiP2Zm5j+rquo/7+7uPzMz8z93d/c/vLv7PwAAAEAiIgJAREQEQGZmBkCJiAhAq6oKQM3MDEDv7g5AERERQDMzE0BVVRVAd3cXQJqZGUC8uxtA3t0dQAAAIEAiIiJAREQkQGZmJkCJiChAq6oqQM3MLEDv7i5AERExQDMzM0BVVTVAd3c3QJqZOUC8uztA3t09QAAAQEAXgyQ7R+G6PwAAAAD9kyg7POK6PwAAAAAv1Sw7DeW6PwAAAAAePzE7oem6PwAAAAA3yjU75e+6PwAAAACybjo7wfe6PwAAAADdJD87IAG7PwAAAAAe5UM76wu7PwAAAACVp0g7Dhi7PwAAAACgZE07byW7PwAAAABVFFI7/DO7PwAAAADrrlY7nUO7PwAAAACkLFs7PFS7PwAAAACvhV87wmW7PwAAAAAwsmM7HHi7PwAAAABpqmc7MIu7PwAAAAB9Zms76p67PwAAAACy3m47NLO7PwAAAAArC3I798e7PwAAAAA+5HQ7Ht27PwAAAAAuYnc7kvK7PwAAAAA1fXk7PQi8PwAAAAC3LXs7Ch68PwAAAAAGbHw74TO8PwAAAACOMH07rkm8PwAAAAC9c307WV+8PwAAAACiz3w7zXS8PwAAAADr9no79Im8PwAAAAAYBHg7t568PwAAAAC0DnQ7AbO8PwAAAAB8K287u8a8PwAAAABBbWk7z9m8PwAAAADk5GI7Key8PwAAAACGoVs7r/28PwAAAACEsVM7Tg69PwAAAAA1IUs77x29PwAAAADV/EE7fCy9PwAAAAAHTzg73Tm9PwAAAACTIi47AEa9PwAAAADUgCM7y1C9PwAAAADKchg7Klq9PwAAAADQAQ07BmK9PwAAAAB2NQE7Smi9PwAAAADXLOo63my9PwAAAAD0V9E6r2+9PwAAAABc/Lc6pHC9PwAAAADRKJ46r2+9PwAAAADE7IM63my9PwAAAADcq1I6Smi9PwAAAABt5xw6BmK9PwAAAACLWM05Klq9PwAAAADgZUA5y1C9PwAAAAB4TNa3AEa9PwAAAABwSHa53Tm9PwAAAABKx+i5fCy9PwAAAADJACu67x29PwAAAABVPmG6Tg69PwAAAACUeIu6r/28PwAAAACy8qW6Key8PwAAAAAB8r+6z9m8PwAAAAAFV9m6u8a8PwAAAAB0/fG6AbO8PwAAAAB23QS7t568PwAAAABeLxC79Im8PwAAAAAZ1hq7zXS8PwAAAAAqrSS7WV+8PwAAAAC0hi27rkm8PwAAAAB3JzW74TO8PwAAAADJQDu7Ch68PwAAAADdZT+7PQi8PwAAAACu90C7kvK7PwAAAABeGkC7Ht27PwAAAADAaz2798e7PwAAAADdyDi7NLO7PwAAAABkDjK76p67PwAAAABbGim7MIu7PwAAAACEzh27HHi7PwAAAACFExC7wmW7PwAAAABeu/+6PFS7PwAAAAA+Y9q6nUO7PwAAAADSVrC6/DO7PwAAAAD9AoK6byW7PwAAAAA0JCC6Dhi7PwAAAABnO1u56wu7PwAAAAA+ZVc5IAG7PwAAAACWiSM6wfe6PwAAAADh/Yc65e+6PwAAAACYkbw6oem6PwAAAABGqO46DeW6PwAAAAAfzw47POK6PwAAAAAXgyQ7R+G6PwAAAAAzvD67AAAAgAAAAAC5/38/FSYAuwAAAIAAAAAA4P9/P23sg7oAAACAAAAAAPj/fz8CC4i4AAAAgAAAAAAAAIA/voxjOgAAAAAAAAAA+v9/P8q06joAAAAAAAAAAOX/fz+6DTE7AAAAAAAAAADD/38/E95rOwAAAAAAAAAAk/9/P3LYkjsAAAAAAAAAAFj/fz/OMq87AAAAAAAAAAAQ/38/Me/KOwAAAAAAAAAAvv5/P+X85TsAAAAAAAAAAGP+fz98JQA8AAAAAAAAAAD//X8/v+QMPAAAAAAAAAAAlP1/PxMzGTwAAAAAAAAAACP9fz+HCCU8AAAAAAAAAACt/H8/W1wwPAAAAAAAAAAANPx/P7smOzwAAAAAAAAAALn7fz+SX0U8AAAAAAAAAAA/+38/V/9OPAAAAAAAAAAAxfp/Pxj/VzwAAAAAAAAAAE76fz9YWGA8AAAAAAAAAADb+X8/agVoPAAAAAAAAAAAbvl/P/4AbzwAAAAAAAAAAAf5fz/dRnU8AAAAAAAAAACo+H8/YdN6PAAAAAAAAAAAUvh/PwOkfzwAAAAAAAAAAAb4fz9S24E8AAAAAAAAAADE938/LoWDPAAAAAAAAAAAjvd/P27PhDwAAAAAAAAAAGP3fz9XuoU8AAAAAAAAAABE938/jEaGPAAAAAAAAAAAMvd/PwV1hjwAAAAAAAAAACz3fz89H4Y8AAAAAAAAAAA3938/LiqFPAAAAAAAAAAAV/d/PwumgzwAAAAAAAAAAIn3fz+noIE8AAAAAAAAAADL938/rkt+PAAAAAAAAAAAG/h/P7Z/eDwAAAAAAAAAAHb4fz+Z73E8AAAAAAAAAADb+H8//qpqPAAAAAAAAAAAR/l/P2rAYjwAAAAAAAAAALn5fz9NPFo8AAAAAAAAAAAw+n8/QCpRPAAAAAAAAAAAqfp/P6aURzwAAAAAAAAAACP7fz8OhT08AAAAAAAAAACe+38/JgQzPAAAAAAAAAAAF/x/PxwaKDwAAAAAAAAAAI38fz9/zhw8AAAAAAAAAAAA/X8/WigRPAAAAAAAAAAAbv1/P5ouBTwAAAAAAAAAANb9fz92zvE7AAAAAAAAAAA3/n8/P7HYOwAAAAAAAAAAkf5/P2sRvzsAAAAAAAAAAOP+fz8a+qQ7AAAAAAAAAAAr/38/LneKOwAAAAAAAAAAav9/P/gmXzsAAAAAAAAAAJ//fz8ftig7AAAAAAAAAADI/38/pWLjOgAAAAAAAAAA5/9/P4vAaDoAAAAAAAAAAPn/fz/b3xI4AAAAAAAAAAAAAIA/CpFXugAAAIAAAAAA+v9/PxOC3LoAAACAAAAAAOj/fz/3rSa7AAAAgAAAAADK/38/JwtfuwAAAIAAAAAAn/9/P0eZi7sAAACAAAAAAGj/fz+Wfqe7AAAAgAAAAAAl/38/iBvDuwAAAIAAAAAA1/5/P19V3rsAAACAAAAAAH7+fz+fCPm7AAAAgAAAAAAb/n8/fIUJvAAAAIAAAAAAsf1/Pz0TFrwAAACAAAAAAED9fz96CSK8AAAAgAAAAADL/H8/kzctvAAAAIAAAAAAVvx/P9NWN7wAAACAAAAAAOb7fz+6+T+8AAAAgAAAAACA+38/MWVGvAAAAIAAAAAAMvt/P50iSbwAAACAAAAAABD7fz+qhUi8AAAAgAAAAAAX+38/+oRGvAAAAIAAAAAAMPt/P+LWQrwAAACAAAAAAF77fz/XIj28AAAAgAAAAACi+38/jQU1vAAAAIAAAAAAAPx/P4YhKrwAAACAAAAAAHf8fz+uRBy8AAAAgAAAAAAF/X8/+aELvAAAAIAAAAAAn/1/P+b28bsAAACAAAAAADf+fz9m+sq7AAAAgAAAAAC+/n8/GKCkuwAAAIAAAAAALP9/P3iAgLsAAACAAAAAAH//fz8zvD67AAAAgAAAAAC5/38/F4MkO0fhuj8AAAAA/ZMoOzziuj8AAAAAL9UsOw3luj8AAAAAHj8xO6Hpuj8AAAAAN8o1O+Xvuj8AAAAAsm46O8H3uj8AAAAA3SQ/OyABuz8AAAAAHuVDO+sLuz8AAAAAladIOw4Yuz8AAAAAoGRNO28luz8AAAAAVRRSO/wzuz8AAAAA665WO51Duz8AAAAApCxbOzxUuz8AAAAAr4VfO8Jluz8AAAAAMLJjOxx4uz8AAAAAaapnOzCLuz8AAAAAfWZrO+qeuz8AAAAAst5uOzSzuz8AAAAAKwtyO/fHuz8AAAAAPuR0Ox7duz8AAAAALmJ3O5Lyuz8AAAAANX15Oz0IvD8AAAAAty17OwoevD8AAAAABmx8O+EzvD8AAAAAjjB9O65JvD8AAAAAvXN9O1lfvD8AAAAAos98O810vD8AAAAA6/Z6O/SJvD8AAAAAGAR4O7eevD8AAAAAtA50OwGzvD8AAAAAfCtvO7vGvD8AAAAAQW1pO8/ZvD8AAAAA5ORiOynsvD8AAAAAhqFbO6/9vD8AAAAAhLFTO04OvT8AAAAANSFLO+8dvT8AAAAA1fxBO3wsvT8AAAAAB084O905vT8AAAAAkyIuOwBGvT8AAAAA1IAjO8tQvT8AAAAAynIYOypavT8AAAAA0AENOwZivT8AAAAAdjUBO0povT8AAAAA1yzqOt5svT8AAAAA9FfROq9vvT8AAAAAXPy3OqRwvT8AAAAA0SieOq9vvT8AAAAAxOyDOt5svT8AAAAA3KtSOkpovT8AAAAAbeccOgZivT8AAAAAi1jNOSpavT8AAAAA4GVAOctQvT8AAAAAeEzWtwBGvT8AAAAAcEh2ud05vT8AAAAASsfouXwsvT8AAAAAyQAruu8dvT8AAAAAVT5huk4OvT8AAAAAlHiLuq/9vD8AAAAAsvKluinsvD8AAAAAAfK/us/ZvD8AAAAABVfZurvGLine truncated
|
||||
@@ -1 +0,0 @@
|
||||
{"asset":{"generator":"FBX2glTF v0.13.1","version":"2.0"},"scene":0,"buffers":[{"byteLength":71248,"uri":"data:application/octet-stream;base64,AAAAAAAAAACJiAg9iYiIPc3MzD2JiAg+q6oqPs3MTD7v7m4+iYiIPpqZmT6rqqo+vLu7Ps3MzD7e3d0+7+7uPgAAAD+JiAg/ERERP5qZGT8iIiI/q6oqPzMzMz+8uzs/REREP83MTD9VVVU/3t1dP2ZmZj/v7m4/d3d3PwAAgD9ERIQ/iYiIP83MjD8REZE/VVWVP5qZmT/e3Z0/IiKiP2Zmpj+rqqo/7+6uPzMzsz93d7c/vLu7PwAAwD9ERMQ/iYjIP83MzD8REdE/VVXVP5qZ2T/e3d0/IiLiP2Zm5j+rquo/7+7uPzMz8z93d/c/vLv7PwAAAEAiIgJAREQEQGZmBkCJiAhAq6oKQM3MDEDv7g5AERERQDMzE0BVVRVAd3cXQJqZGUC8uxtA3t0dQAAAIEAiIiJAREQkQGZmJkCJiChAq6oqQM3MLEDv7i5AERExQDMzM0BVVTVAd3c3QJqZOUC8uztA3t09QAAAQEAXgyQ7R+G6PwAAAAD9kyg7POK6PwAAAAAv1Sw7DeW6PwAAAAAePzE7oem6PwAAAAA3yjU75e+6PwAAAACybjo7wfe6PwAAAADdJD87IAG7PwAAAAAe5UM76wu7PwAAAACVp0g7Dhi7PwAAAACgZE07byW7PwAAAABVFFI7/DO7PwAAAADrrlY7nUO7PwAAAACkLFs7PFS7PwAAAACvhV87wmW7PwAAAAAwsmM7HHi7PwAAAABpqmc7MIu7PwAAAAB9Zms76p67PwAAAACy3m47NLO7PwAAAAArC3I798e7PwAAAAA+5HQ7Ht27PwAAAAAuYnc7kvK7PwAAAAA1fXk7PQi8PwAAAAC3LXs7Ch68PwAAAAAGbHw74TO8PwAAAACOMH07rkm8PwAAAAC9c307WV+8PwAAAACiz3w7zXS8PwAAAADr9no79Im8PwAAAAAYBHg7t568PwAAAAC0DnQ7AbO8PwAAAAB8K287u8a8PwAAAABBbWk7z9m8PwAAAADk5GI7Key8PwAAAACGoVs7r/28PwAAAACEsVM7Tg69PwAAAAA1IUs77x29PwAAAADV/EE7fCy9PwAAAAAHTzg73Tm9PwAAAACTIi47AEa9PwAAAADUgCM7y1C9PwAAAADKchg7Klq9PwAAAADQAQ07BmK9PwAAAAB2NQE7Smi9PwAAAADXLOo63my9PwAAAAD0V9E6r2+9PwAAAABc/Lc6pHC9PwAAAADRKJ46r2+9PwAAAADE7IM63my9PwAAAADcq1I6Smi9PwAAAABt5xw6BmK9PwAAAACLWM05Klq9PwAAAADgZUA5y1C9PwAAAAB4TNa3AEa9PwAAAABwSHa53Tm9PwAAAABKx+i5fCy9PwAAAADJACu67x29PwAAAABVPmG6Tg69PwAAAACUeIu6r/28PwAAAACy8qW6Key8PwAAAAAB8r+6z9m8PwAAAAAFV9m6u8a8PwAAAAB0/fG6AbO8PwAAAAB23QS7t568PwAAAABeLxC79Im8PwAAAAAZ1hq7zXS8PwAAAAAqrSS7WV+8PwAAAAC0hi27rkm8PwAAAAB3JzW74TO8PwAAAADJQDu7Ch68PwAAAADdZT+7PQi8PwAAAACu90C7kvK7PwAAAABeGkC7Ht27PwAAAADAaz2798e7PwAAAADdyDi7NLO7PwAAAABkDjK76p67PwAAAABbGim7MIu7PwAAAACEzh27HHi7PwAAAACFExC7wmW7PwAAAABeu/+6PFS7PwAAAAA+Y9q6nUO7PwAAAADSVrC6/DO7PwAAAAD9AoK6byW7PwAAAAA0JCC6Dhi7PwAAAABnO1u56wu7PwAAAAA+ZVc5IAG7PwAAAACWiSM6wfe6PwAAAADh/Yc65e+6PwAAAACYkbw6oem6PwAAAABGqO46DeW6PwAAAAAfzw47POK6PwAAAAAXgyQ7R+G6PwAAAAAzvD67AAAAgAAAAAC5/38/FSYAuwAAAIAAAAAA4P9/P23sg7oAAACAAAAAAPj/fz8CC4i4AAAAgAAAAAAAAIA/voxjOgAAAAAAAAAA+v9/P8q06joAAAAAAAAAAOX/fz+6DTE7AAAAAAAAAADD/38/E95rOwAAAAAAAAAAk/9/P3LYkjsAAAAAAAAAAFj/fz/OMq87AAAAAAAAAAAQ/38/Me/KOwAAAAAAAAAAvv5/P+X85TsAAAAAAAAAAGP+fz98JQA8AAAAAAAAAAD//X8/v+QMPAAAAAAAAAAAlP1/PxMzGTwAAAAAAAAAACP9fz+HCCU8AAAAAAAAAACt/H8/W1wwPAAAAAAAAAAANPx/P7smOzwAAAAAAAAAALn7fz+SX0U8AAAAAAAAAAA/+38/V/9OPAAAAAAAAAAAxfp/Pxj/VzwAAAAAAAAAAE76fz9YWGA8AAAAAAAAAADb+X8/agVoPAAAAAAAAAAAbvl/P/4AbzwAAAAAAAAAAAf5fz/dRnU8AAAAAAAAAACo+H8/YdN6PAAAAAAAAAAAUvh/PwOkfzwAAAAAAAAAAAb4fz9S24E8AAAAAAAAAADE938/LoWDPAAAAAAAAAAAjvd/P27PhDwAAAAAAAAAAGP3fz9XuoU8AAAAAAAAAABE938/jEaGPAAAAAAAAAAAMvd/PwV1hjwAAAAAAAAAACz3fz89H4Y8AAAAAAAAAAA3938/LiqFPAAAAAAAAAAAV/d/PwumgzwAAAAAAAAAAIn3fz+noIE8AAAAAAAAAADL938/rkt+PAAAAAAAAAAAG/h/P7Z/eDwAAAAAAAAAAHb4fz+Z73E8AAAAAAAAAADb+H8//qpqPAAAAAAAAAAAR/l/P2rAYjwAAAAAAAAAALn5fz9NPFo8AAAAAAAAAAAw+n8/QCpRPAAAAAAAAAAAqfp/P6aURzwAAAAAAAAAACP7fz8OhT08AAAAAAAAAACe+38/JgQzPAAAAAAAAAAAF/x/PxwaKDwAAAAAAAAAAI38fz9/zhw8AAAAAAAAAAAA/X8/WigRPAAAAAAAAAAAbv1/P5ouBTwAAAAAAAAAANb9fz92zvE7AAAAAAAAAAA3/n8/P7HYOwAAAAAAAAAAkf5/P2sRvzsAAAAAAAAAAOP+fz8a+qQ7AAAAAAAAAAAr/38/LneKOwAAAAAAAAAAav9/P/gmXzsAAAAAAAAAAJ//fz8ftig7AAAAAAAAAADI/38/pWLjOgAAAAAAAAAA5/9/P4vAaDoAAAAAAAAAAPn/fz/b3xI4AAAAAAAAAAAAAIA/CpFXugAAAIAAAAAA+v9/PxOC3LoAAACAAAAAAOj/fz/3rSa7AAAAgAAAAADK/38/JwtfuwAAAIAAAAAAn/9/P0eZi7sAAACAAAAAAGj/fz+Wfqe7AAAAgAAAAAAl/38/iBvDuwAAAIAAAAAA1/5/P19V3rsAAACAAAAAAH7+fz+fCPm7AAAAgAAAAAAb/n8/fIUJvAAAAIAAAAAAsf1/Pz0TFrwAAACAAAAAAED9fz96CSK8AAAAgAAAAADL/H8/kzctvAAAAIAAAAAAVvx/P9NWN7wAAACAAAAAAOb7fz+6+T+8AAAAgAAAAACA+38/MWVGvAAAAIAAAAAAMvt/P50iSbwAAACAAAAAABD7fz+qhUi8AAAAgAAAAAAX+38/+oRGvAAAAIAAAAAAMPt/P+LWQrwAAACAAAAAAF77fz/XIj28AAAAgAAAAACi+38/jQU1vAAAAIAAAAAAAPx/P4YhKrwAAACAAAAAAHf8fz+uRBy8AAAAgAAAAAAF/X8/+aELvAAAAIAAAAAAn/1/P+b28bsAAACAAAAAADf+fz9m+sq7AAAAgAAAAAC+/n8/GKCkuwAAAIAAAAAALP9/P3iAgLsAAACAAAAAAH//fz8zvD67AAAAgAAAAAC5/38/F4MkO0fhuj8AAAAA/ZMoOzziuj8AAAAAL9UsOw3luj8AAAAAHj8xO6Hpuj8AAAAAN8o1O+Xvuj8AAAAAsm46O8H3uj8AAAAA3SQ/OyABuz8AAAAAHuVDO+sLuz8AAAAAladIOw4Yuz8AAAAAoGRNO28luz8AAAAAVRRSO/wzuz8AAAAA665WO51Duz8AAAAApCxbOzxUuz8AAAAAr4VfO8Jluz8AAAAAMLJjOxx4uz8AAAAAaapnOzCLuz8AAAAAfWZrO+qeuz8AAAAAst5uOzSzuz8AAAAAKwtyO/fHuz8AAAAAPuR0Ox7duz8AAAAALmJ3O5Lyuz8AAAAANX15Oz0IvD8AAAAAty17OwoevD8AAAAABmx8O+EzvD8AAAAAjjB9O65JvD8AAAAAvXN9O1lfvD8AAAAAos98O810vD8AAAAA6/Z6O/SJvD8AAAAAGAR4O7eevD8AAAAAtA50OwGzvD8AAAAAfCtvO7vGvD8AAAAAQW1pO8/ZvD8AAAAA5ORiOynsvD8AAAAAhqFbO6/9vD8AAAAAhLFTO04OvT8AAAAANSFLO+8dvT8AAAAA1fxBO3wsvT8AAAAAB084O905vT8AAAAAkyIuOwBGvT8AAAAA1IAjO8tQvT8AAAAAynIYOypavT8AAAAA0AENOwZivT8AAAAAdjUBO0povT8AAAAA1yzqOt5svT8AAAAA9FfROq9vvT8AAAAAXPy3OqRwvT8AAAAA0SieOq9vvT8AAAAAxOyDOt5svT8AAAAA3KtSOkpovT8AAAAAbeccOgZivT8AAAAAi1jNOSpavT8AAAAA4GVAOctQvT8AAAAAeEzWtwBGvT8AAAAAcEh2ud05vT8AAAAASsfouXwsvT8AAAAAyQAruu8dvT8AAAAAVT5huk4OvT8AAAAAlHiLuq/9vD8AAAAAsvKluinsvD8AAAAAAfK/us/ZvD8AAAAABVfZurvGLine truncated
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -9,7 +9,9 @@
|
||||
<Avatar
|
||||
size="36px"
|
||||
:src="
|
||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
selectedAccount
|
||||
? `https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`
|
||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col w-full">
|
||||
@@ -26,7 +28,7 @@
|
||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||
>
|
||||
<div v-if="selectedAccount" class="selected account">
|
||||
<Avatar size="xs" :src="avatarUrl" />
|
||||
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`" />
|
||||
<div>
|
||||
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||
<p>Selected</p>
|
||||
@@ -42,21 +44,14 @@
|
||||
</div>
|
||||
<div v-else class="logged-out account">
|
||||
<h4>Not signed in</h4>
|
||||
<Button
|
||||
v-tooltip="'Log in'"
|
||||
:disabled="loginDisabled"
|
||||
icon-only
|
||||
color="primary"
|
||||
@click="login()"
|
||||
>
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
<Button v-tooltip="'Log in'" icon-only color="primary" @click="login()">
|
||||
<LogInIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||
<Button class="option account" @click="setAccount(account)">
|
||||
<Avatar :src="getAccountAvatarUrl(account)" class="icon" />
|
||||
<Avatar :src="`https://mc-heads.net/avatar/${account.profile.id}/128`" class="icon" />
|
||||
<p>{{ account.profile.name }}</p>
|
||||
</Button>
|
||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||
@@ -73,7 +68,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||
import {
|
||||
@@ -87,8 +82,6 @@ import { handleError } from '@/store/state.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { get_available_skins } from '@/helpers/skins'
|
||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
@@ -101,40 +94,14 @@ defineProps({
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const accounts = ref({})
|
||||
const loginDisabled = ref(false)
|
||||
const defaultUser = ref()
|
||||
const equippedSkin = ref(null)
|
||||
const headUrlCache = ref(new Map())
|
||||
|
||||
async function refreshValues() {
|
||||
defaultUser.value = await get_default_user().catch(handleError)
|
||||
accounts.value = await users().catch(handleError)
|
||||
|
||||
try {
|
||||
const skins = await get_available_skins()
|
||||
equippedSkin.value = skins.find((skin) => skin.is_equipped)
|
||||
|
||||
if (equippedSkin.value) {
|
||||
try {
|
||||
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
|
||||
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
|
||||
} catch (error) {
|
||||
console.warn('Failed to get head render for equipped skin:', error)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
equippedSkin.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginDisabled(value) {
|
||||
loginDisabled.value = value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refreshValues,
|
||||
setLoginDisabled,
|
||||
loginDisabled,
|
||||
})
|
||||
await refreshValues()
|
||||
|
||||
@@ -142,33 +109,6 @@ const displayAccounts = computed(() =>
|
||||
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||
)
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
if (equippedSkin.value?.texture_key) {
|
||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||
if (cachedUrl) {
|
||||
return cachedUrl
|
||||
}
|
||||
return `https://mc-heads.net/avatar/${equippedSkin.value.texture_key}/128`
|
||||
}
|
||||
if (selectedAccount.value?.profile?.id) {
|
||||
return `https://mc-heads.net/avatar/${selectedAccount.value.profile.id}/128`
|
||||
}
|
||||
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
})
|
||||
|
||||
function getAccountAvatarUrl(account) {
|
||||
if (
|
||||
account.profile.id === selectedAccount.value?.profile?.id &&
|
||||
equippedSkin.value?.texture_key
|
||||
) {
|
||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||
if (cachedUrl) {
|
||||
return cachedUrl
|
||||
}
|
||||
}
|
||||
return `https://mc-heads.net/avatar/${account.profile.id}/128`
|
||||
}
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
@@ -180,7 +120,6 @@ async function setAccount(account) {
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginDisabled.value = true
|
||||
const loggedIn = await login_flow().catch(handleSevereError)
|
||||
|
||||
if (loggedIn) {
|
||||
@@ -189,7 +128,6 @@ async function login() {
|
||||
}
|
||||
|
||||
trackEvent('AccountLogIn')
|
||||
loginDisabled.value = false
|
||||
}
|
||||
|
||||
const logout = async (id) => {
|
||||
|
||||
@@ -219,8 +219,8 @@ async function copyToClipboard(text) {
|
||||
<template v-else-if="metadata.notEnoughSpace">
|
||||
<h3>Not enough space</h3>
|
||||
<p>
|
||||
It looks like there is not enough space on the disk containing the directory you
|
||||
selected. Please free up some space and try again or cancel the directory change.
|
||||
It looks like there is not enough space on the disk containing the dirctory you
|
||||
selected Please free up some space and try again or cancel the directory change.
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import dayjs from 'dayjs'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
@@ -236,8 +237,8 @@ onUnmounted(() => unlisten())
|
||||
</p>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||
<GameIcon class="shrink-0" />
|
||||
<span class="text-sm capitalize">
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
<span class="text-sm">
|
||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +127,7 @@ async function handleJavaFileInput() {
|
||||
const filePath = await open()
|
||||
|
||||
if (filePath) {
|
||||
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
||||
let result = await get_jre(filePath.path ?? filePath)
|
||||
if (!result) {
|
||||
result = {
|
||||
path: filePath.path ?? filePath,
|
||||
|
||||
@@ -32,33 +32,8 @@ function updateAdPosition() {
|
||||
|
||||
<template>
|
||||
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
|
||||
<a
|
||||
href="https://modrinth.gg?from=app-placeholder"
|
||||
target="_blank"
|
||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
|
||||
>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-light.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="hidden light-image rounded-[inherit]"
|
||||
/>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-dark.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="dark-image rounded-[inherit]"
|
||||
/>
|
||||
</a>
|
||||
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
|
||||
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.light,
|
||||
.light-mode {
|
||||
.dark-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.light-image {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ShieldIcon,
|
||||
SettingsIcon,
|
||||
GaugeIcon,
|
||||
PaintbrushIcon,
|
||||
PaintBrushIcon,
|
||||
GameIcon,
|
||||
CoffeeIcon,
|
||||
} from '@modrinth/assets'
|
||||
@@ -41,7 +41,7 @@ const tabs = [
|
||||
id: 'app.settings.tabs.appearance',
|
||||
defaultMessage: 'Appearance',
|
||||
}),
|
||||
icon: PaintbrushIcon,
|
||||
icon: PaintBrushIcon,
|
||||
content: AppearanceSettings,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { NewModal as Modal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
@@ -26,16 +26,16 @@ const props = defineProps({
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
const modal = useTemplateRef('modal')
|
||||
const modal = ref(null)
|
||||
|
||||
defineExpose({
|
||||
show: (e: MouseEvent) => {
|
||||
show: () => {
|
||||
hide_ads_window()
|
||||
modal.value?.show(e)
|
||||
modal.value.show()
|
||||
},
|
||||
hide: () => {
|
||||
onModalHide()
|
||||
modal.value?.hide()
|
||||
modal.value.hide()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -56,17 +56,9 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page. page.</p>
|
||||
</div>
|
||||
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
||||
</div>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native Decorations</h2>
|
||||
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||
</div>
|
||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||
|
||||
@@ -1,420 +0,0 @@
|
||||
<script lang="ts">
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
</script>
|
||||
<template>
|
||||
<UploadSkinModal ref="uploadModal" />
|
||||
<ModalWrapper ref="modal" @on-hide="resetState">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
:slim-model-src="slimModelUrl"
|
||||
:wide-model-src="wideModelUrl"
|
||||
:cape-model-src="capeModelUrl"
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin || ''"
|
||||
:cape-src="selectedCapeTexture"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
||||
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Arm style</h2>
|
||||
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
|
||||
<template #default="{ item }">
|
||||
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
|
||||
</template>
|
||||
</RadioButtons>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Cape</h2>
|
||||
<div class="flex gap-2">
|
||||
<CapeButton
|
||||
v-if="defaultCape"
|
||||
:id="defaultCape.id"
|
||||
:texture="defaultCape.texture"
|
||||
:name="undefined"
|
||||
:selected="!selectedCape"
|
||||
faded
|
||||
@select="selectCape(undefined)"
|
||||
>
|
||||
<span>Use default cape</span>
|
||||
</CapeButton>
|
||||
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
|
||||
<span>Use default cape</span>
|
||||
</CapeLikeTextButton>
|
||||
|
||||
<CapeButton
|
||||
v-for="cape in visibleCapeList"
|
||||
:id="cape.id"
|
||||
:key="cape.id"
|
||||
:texture="cape.texture"
|
||||
:name="cape.name || 'Cape'"
|
||||
:selected="selectedCape?.id === cape.id"
|
||||
@select="selectCape(cape)"
|
||||
/>
|
||||
|
||||
<CapeLikeTextButton
|
||||
v-if="(capes?.length ?? 0) > 2"
|
||||
tooltip="View more capes"
|
||||
@mouseup="openSelectCapeModal"
|
||||
>
|
||||
<template #icon><ChevronRightIcon /></template>
|
||||
<span>More</span>
|
||||
</CapeLikeTextButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-12">
|
||||
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
<SelectCapeModal
|
||||
ref="selectCapeModal"
|
||||
:capes="capes || []"
|
||||
@select="handleCapeSelected"
|
||||
@cancel="handleCapeCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, useTemplateRef } from 'vue'
|
||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||
import {
|
||||
SkinPreviewRenderer,
|
||||
Button,
|
||||
RadioButtons,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
ButtonStyled,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
add_and_equip_custom_skin,
|
||||
remove_custom_skin,
|
||||
unequip_skin,
|
||||
type Skin,
|
||||
type Cape,
|
||||
type SkinModel,
|
||||
get_normalized_skin_texture,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import {
|
||||
UploadIcon,
|
||||
CheckIcon,
|
||||
SaveIcon,
|
||||
XIcon,
|
||||
ChevronRightIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
const mode = ref<'new' | 'edit'>('new')
|
||||
const currentSkin = ref<Skin | null>(null)
|
||||
const shouldRestoreModal = ref(false)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const uploadedTextureUrl = ref<string | null>(null)
|
||||
const previewSkin = ref<string>('')
|
||||
|
||||
const variant = ref<SkinModel>('CLASSIC')
|
||||
const selectedCape = ref<Cape | undefined>(undefined)
|
||||
const props = defineProps<{ capes?: Cape[]; defaultCape?: Cape }>()
|
||||
|
||||
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
|
||||
const visibleCapeList = ref<Cape[]>([])
|
||||
|
||||
const sortedCapes = computed(() => {
|
||||
return [...(props.capes || [])].sort((a, b) => {
|
||||
const nameA = (a.name || '').toLowerCase()
|
||||
const nameB = (b.name || '').toLowerCase()
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
})
|
||||
|
||||
function initVisibleCapeList() {
|
||||
if (!props.capes || props.capes.length === 0) {
|
||||
visibleCapeList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (visibleCapeList.value.length === 0) {
|
||||
if (selectedCape.value) {
|
||||
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
|
||||
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
|
||||
} else {
|
||||
visibleCapeList.value = getSortedCapes(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSortedCapes(count: number): Cape[] {
|
||||
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
|
||||
return sortedCapes.value.slice(0, count)
|
||||
}
|
||||
|
||||
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
||||
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
|
||||
return sortedCapes.value.find((cape) => cape.id !== excludeId)
|
||||
}
|
||||
|
||||
async function loadPreviewSkin() {
|
||||
if (uploadedTextureUrl.value) {
|
||||
previewSkin.value = uploadedTextureUrl.value
|
||||
} else if (currentSkin.value) {
|
||||
try {
|
||||
previewSkin.value = await get_normalized_skin_texture(currentSkin.value)
|
||||
} catch (error) {
|
||||
console.error('Failed to load skin texture:', error)
|
||||
previewSkin.value = '/src/assets/skins/steve.png'
|
||||
}
|
||||
} else {
|
||||
previewSkin.value = '/src/assets/skins/steve.png'
|
||||
}
|
||||
}
|
||||
|
||||
const hasEdits = computed(() => {
|
||||
if (mode.value !== 'edit') return true
|
||||
if (uploadedTextureUrl.value) return true
|
||||
if (!currentSkin.value) return false
|
||||
if (variant.value !== currentSkin.value.variant) return true
|
||||
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const disableSave = computed(
|
||||
() =>
|
||||
(mode.value === 'new' && !uploadedTextureUrl.value) ||
|
||||
(mode.value === 'edit' && !hasEdits.value),
|
||||
)
|
||||
|
||||
const saveTooltip = computed(() => {
|
||||
if (isSaving.value) return 'Saving...'
|
||||
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
||||
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
||||
return undefined
|
||||
})
|
||||
|
||||
function resetState() {
|
||||
mode.value = 'new'
|
||||
currentSkin.value = null
|
||||
uploadedTextureUrl.value = null
|
||||
previewSkin.value = ''
|
||||
variant.value = 'CLASSIC'
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
shouldRestoreModal.value = false
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
async function show(e: MouseEvent, skin?: Skin) {
|
||||
mode.value = skin ? 'edit' : 'new'
|
||||
currentSkin.value = skin ?? null
|
||||
if (skin) {
|
||||
variant.value = skin.variant
|
||||
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
|
||||
} else {
|
||||
variant.value = 'CLASSIC'
|
||||
selectedCape.value = undefined
|
||||
}
|
||||
visibleCapeList.value = []
|
||||
initVisibleCapeList()
|
||||
|
||||
await loadPreviewSkin()
|
||||
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
||||
mode.value = 'new'
|
||||
currentSkin.value = null
|
||||
uploadedTextureUrl.value = skinTextureUrl
|
||||
variant.value = 'CLASSIC'
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
initVisibleCapeList()
|
||||
|
||||
await loadPreviewSkin()
|
||||
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
async function restoreWithNewTexture(skinTextureUrl: string) {
|
||||
uploadedTextureUrl.value = skinTextureUrl
|
||||
await loadPreviewSkin()
|
||||
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
setTimeout(() => resetState(), 250)
|
||||
}
|
||||
|
||||
function selectCape(cape: Cape | undefined) {
|
||||
if (cape && selectedCape.value?.id !== cape.id) {
|
||||
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
|
||||
if (!isInVisibleList && visibleCapeList.value.length > 0) {
|
||||
visibleCapeList.value.splice(0, 1, cape)
|
||||
|
||||
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
|
||||
const otherCape = getSortedCapeExcluding(cape.id)
|
||||
if (otherCape) {
|
||||
visibleCapeList.value.splice(1, 1, otherCape)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedCape.value = cape
|
||||
}
|
||||
|
||||
function handleCapeSelected(cape: Cape | undefined) {
|
||||
selectCape(cape)
|
||||
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCapeCancel() {
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function openSelectCapeModal(e: MouseEvent) {
|
||||
if (!selectCapeModal.value) return
|
||||
|
||||
shouldRestoreModal.value = true
|
||||
modal.value?.hide()
|
||||
|
||||
setTimeout(() => {
|
||||
selectCapeModal.value?.show(
|
||||
e,
|
||||
currentSkin.value?.texture_key,
|
||||
selectedCape.value,
|
||||
previewSkin.value,
|
||||
variant.value,
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function openUploadSkinModal(e: MouseEvent) {
|
||||
shouldRestoreModal.value = true
|
||||
modal.value?.hide()
|
||||
emit('open-upload-modal', e)
|
||||
}
|
||||
|
||||
function restoreModal() {
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
const fakeEvent = new MouseEvent('click')
|
||||
modal.value?.show(fakeEvent)
|
||||
shouldRestoreModal.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
isSaving.value = true
|
||||
|
||||
try {
|
||||
let textureUrl: string
|
||||
|
||||
if (uploadedTextureUrl.value) {
|
||||
textureUrl = uploadedTextureUrl.value
|
||||
} else {
|
||||
textureUrl = currentSkin.value!.texture
|
||||
}
|
||||
|
||||
await unequip_skin()
|
||||
|
||||
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
|
||||
|
||||
if (mode.value === 'new') {
|
||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||
emit('saved')
|
||||
} else {
|
||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||
await remove_custom_skin(currentSkin.value!)
|
||||
emit('saved')
|
||||
}
|
||||
|
||||
hide()
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch([uploadedTextureUrl, currentSkin], async () => {
|
||||
await loadPreviewSkin()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.capes,
|
||||
() => {
|
||||
initVisibleCapeList()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'saved'): void
|
||||
(event: 'deleted', skin: Skin): void
|
||||
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
|
||||
}>()
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
showNew,
|
||||
restoreWithNewTexture,
|
||||
hide,
|
||||
shouldRestoreModal,
|
||||
restoreModal,
|
||||
})
|
||||
</script>
|
||||
@@ -1,146 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef, ref, computed } from 'vue'
|
||||
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
||||
import {
|
||||
ButtonStyled,
|
||||
ScrollablePanel,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
SkinPreviewRenderer,
|
||||
} from '@modrinth/ui'
|
||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', cape: Cape | undefined): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
capes: Cape[]
|
||||
}>()
|
||||
|
||||
const sortedCapes = computed(() => {
|
||||
return [...props.capes].sort((a, b) => {
|
||||
const nameA = (a.name || '').toLowerCase()
|
||||
const nameB = (b.name || '').toLowerCase()
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
})
|
||||
|
||||
const currentSkinId = ref<string | undefined>()
|
||||
const currentSkinTexture = ref<string | undefined>()
|
||||
const currentSkinVariant = ref<SkinModel>('CLASSIC')
|
||||
const currentCapeTexture = computed<string | undefined>(() => currentCape.value?.texture)
|
||||
const currentCape = ref<Cape | undefined>()
|
||||
|
||||
function show(
|
||||
e: MouseEvent,
|
||||
skinId?: string,
|
||||
selected?: Cape,
|
||||
skinTexture?: string,
|
||||
variant?: SkinModel,
|
||||
) {
|
||||
currentSkinId.value = skinId
|
||||
currentSkinTexture.value = skinTexture
|
||||
currentSkinVariant.value = variant || 'CLASSIC'
|
||||
currentCape.value = selected
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
function select() {
|
||||
emit('select', currentCape.value)
|
||||
hide()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function updateSelectedCape(cape: Cape | undefined) {
|
||||
currentCape.value = cape
|
||||
}
|
||||
|
||||
function onModalHide() {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper ref="modal" @on-hide="onModalHide">
|
||||
<template #title>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-lg font-extrabold text-heading">Change cape</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
v-if="currentSkinTexture"
|
||||
:slim-model-src="slimModelUrl"
|
||||
:wide-model-src="wideModelUrl"
|
||||
:cape-model-src="capeModelUrl"
|
||||
:cape-src="currentCapeTexture"
|
||||
:texture-src="currentSkinTexture"
|
||||
:variant="currentSkinVariant"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
:initial-rotation="Math.PI + Math.PI / 8"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full my-auto">
|
||||
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
|
||||
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
|
||||
<CapeLikeTextButton
|
||||
tooltip="No Cape"
|
||||
:highlighted="!currentCape"
|
||||
@click="updateSelectedCape(undefined)"
|
||||
>
|
||||
<template #icon>
|
||||
<XIcon />
|
||||
</template>
|
||||
<span>None</span>
|
||||
</CapeLikeTextButton>
|
||||
<CapeButton
|
||||
v-for="cape in sortedCapes"
|
||||
:id="cape.id"
|
||||
:key="cape.id"
|
||||
:name="cape.name"
|
||||
:texture="cape.texture"
|
||||
:selected="currentCape?.id === cape.id"
|
||||
@select="updateSelectedCape(cape)"
|
||||
/>
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="select">
|
||||
<CheckIcon />
|
||||
Select
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<ModalWrapper ref="modal" @on-hide="hide(true)">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
|
||||
</template>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
|
||||
<UploadIcon /> Select skin texture file
|
||||
</p>
|
||||
<p class="mx-auto mt-0 text-secondary text-sm text-center">
|
||||
Drag and drop or click here to browse
|
||||
</p>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/png"
|
||||
class="hidden"
|
||||
@change="handleInputFileChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { UploadIcon } from '@modrinth/assets'
|
||||
import { useNotifications } from '@/store/state'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get_dragged_skin_data } from '@/helpers/skins'
|
||||
|
||||
const notifications = useNotifications()
|
||||
|
||||
const modal = ref()
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const unlisten = ref<() => void>()
|
||||
const modalVisible = ref(false)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'uploaded', data: ArrayBuffer): void
|
||||
(e: 'canceled'): void
|
||||
}>()
|
||||
|
||||
function show(e?: MouseEvent) {
|
||||
modal.value?.show(e)
|
||||
modalVisible.value = true
|
||||
setupDragDropListener()
|
||||
}
|
||||
|
||||
function hide(emitCanceled = false) {
|
||||
modal.value?.hide()
|
||||
modalVisible.value = false
|
||||
cleanupDragDropListener()
|
||||
resetState()
|
||||
if (emitCanceled) {
|
||||
emit('canceled')
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
}
|
||||
|
||||
function triggerFileInput() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleInputFileChange(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files || files.length === 0) {
|
||||
return
|
||||
}
|
||||
const file = files[0]
|
||||
const buffer = await file.arrayBuffer()
|
||||
await processData(buffer)
|
||||
}
|
||||
|
||||
async function setupDragDropListener() {
|
||||
try {
|
||||
if (modalVisible.value) {
|
||||
await cleanupDragDropListener()
|
||||
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
if (event.payload.type !== 'drop') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.payload.paths || event.payload.paths.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const filePath = event.payload.paths[0]
|
||||
|
||||
try {
|
||||
const data = await get_dragged_skin_data(filePath)
|
||||
await processData(data.buffer)
|
||||
} catch (error) {
|
||||
notifications.addNotification({
|
||||
title: 'Error processing file',
|
||||
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set up drag and drop listener:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupDragDropListener() {
|
||||
if (unlisten.value) {
|
||||
unlisten.value()
|
||||
unlisten.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function processData(buffer: ArrayBuffer) {
|
||||
emit('uploaded', buffer)
|
||||
hide()
|
||||
}
|
||||
|
||||
watch(modalVisible, (isVisible) => {
|
||||
if (isVisible) {
|
||||
setupDragDropListener()
|
||||
} else {
|
||||
cleanupDragDropListener()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupDragDropListener()
|
||||
})
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
@@ -1,12 +1,12 @@
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { ofetch } from 'ofetch'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
|
||||
export const useFetch = async (url, item, isSilent) => {
|
||||
try {
|
||||
const version = await getVersion()
|
||||
return await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
return await ofetch(url, {
|
||||
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
import * as THREE from 'three'
|
||||
import type { Skin, Cape } from '../skins'
|
||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
||||
import { reactive } from 'vue'
|
||||
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
|
||||
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
|
||||
export interface RenderResult {
|
||||
forwards: string
|
||||
backwards: string
|
||||
}
|
||||
|
||||
class BatchSkinRenderer {
|
||||
private renderer: THREE.WebGLRenderer
|
||||
private readonly scene: THREE.Scene
|
||||
private readonly camera: THREE.PerspectiveCamera
|
||||
private currentModel: THREE.Group | null = null
|
||||
|
||||
constructor(width: number = 360, height: number = 504) {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
preserveDrawingBuffer: true,
|
||||
})
|
||||
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
this.renderer.toneMappingExposure = 10.0
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.setSize(width, height)
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(20, width / height, 0.4, 1000)
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.position.set(2, 4, 3)
|
||||
this.scene.add(ambientLight)
|
||||
this.scene.add(directionalLight)
|
||||
}
|
||||
|
||||
public async renderSkin(
|
||||
textureUrl: string,
|
||||
modelUrl: string,
|
||||
capeUrl?: string,
|
||||
capeModelUrl?: string,
|
||||
): Promise<RenderResult> {
|
||||
await this.setupModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
||||
|
||||
const headPart = this.currentModel!.getObjectByName('Head')
|
||||
let lookAtTarget: [number, number, number]
|
||||
|
||||
if (headPart) {
|
||||
const headPosition = new THREE.Vector3()
|
||||
headPart.getWorldPosition(headPosition)
|
||||
lookAtTarget = [headPosition.x, headPosition.y - 0.3, headPosition.z]
|
||||
} else {
|
||||
throw new Error("Failed to find 'Head' object in model.")
|
||||
}
|
||||
|
||||
const frontCameraPos: [number, number, number] = [-1.3, 1, 6.3]
|
||||
const backCameraPos: [number, number, number] = [-1.3, 1, -2.5]
|
||||
|
||||
const forwards = await this.renderView(frontCameraPos, lookAtTarget)
|
||||
const backwards = await this.renderView(backCameraPos, lookAtTarget)
|
||||
|
||||
return { forwards, backwards }
|
||||
}
|
||||
|
||||
private async renderView(
|
||||
cameraPosition: [number, number, number],
|
||||
lookAtPosition: [number, number, number],
|
||||
): Promise<string> {
|
||||
this.camera.position.set(...cameraPosition)
|
||||
this.camera.lookAt(...lookAtPosition)
|
||||
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.renderer.domElement.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
resolve(url)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
}, 'image/png')
|
||||
})
|
||||
}
|
||||
|
||||
private async setupModel(
|
||||
modelUrl: string,
|
||||
textureUrl: string,
|
||||
capeModelUrl?: string,
|
||||
capeUrl?: string,
|
||||
): Promise<void> {
|
||||
if (this.currentModel) {
|
||||
this.scene.remove(this.currentModel)
|
||||
}
|
||||
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.add(model)
|
||||
group.position.set(0, 0.3, 1.95)
|
||||
group.scale.set(0.8, 0.8, 0.8)
|
||||
|
||||
this.scene.add(group)
|
||||
this.currentModel = group
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.renderer.dispose()
|
||||
disposeCaches()
|
||||
}
|
||||
}
|
||||
|
||||
function getModelUrlForVariant(variant: string): string {
|
||||
switch (variant) {
|
||||
case 'SLIM':
|
||||
return slimModelUrl
|
||||
case 'CLASSIC':
|
||||
case 'UNKNOWN':
|
||||
default:
|
||||
return wideModelUrl
|
||||
}
|
||||
}
|
||||
|
||||
export const map = reactive(new Map<string, RenderResult>())
|
||||
export const headMap = reactive(new Map<string, string>())
|
||||
const DEBUG_MODE = false
|
||||
|
||||
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
const validKeys = new Set<string>()
|
||||
const validHeadKeys = new Set<string>()
|
||||
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
validKeys.add(key)
|
||||
validHeadKeys.add(headKey)
|
||||
}
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys)
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup unused skin previews:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const sourceCanvas = document.createElement('canvas')
|
||||
const sourceCtx = sourceCanvas.getContext('2d')
|
||||
|
||||
if (!sourceCtx) {
|
||||
throw new Error('Could not get 2D context from source canvas')
|
||||
}
|
||||
|
||||
sourceCanvas.width = img.width
|
||||
sourceCanvas.height = img.height
|
||||
|
||||
sourceCtx.drawImage(img, 0, 0)
|
||||
|
||||
const outputCanvas = document.createElement('canvas')
|
||||
const outputCtx = outputCanvas.getContext('2d')
|
||||
|
||||
if (!outputCtx) {
|
||||
throw new Error('Could not get 2D context from output canvas')
|
||||
}
|
||||
|
||||
outputCanvas.width = size
|
||||
outputCanvas.height = size
|
||||
|
||||
outputCtx.imageSmoothingEnabled = false
|
||||
|
||||
const headImageData = sourceCtx.getImageData(8, 8, 8, 8)
|
||||
|
||||
const headCanvas = document.createElement('canvas')
|
||||
const headCtx = headCanvas.getContext('2d')
|
||||
|
||||
if (!headCtx) {
|
||||
throw new Error('Could not get 2D context from head canvas')
|
||||
}
|
||||
|
||||
headCanvas.width = 8
|
||||
headCanvas.height = 8
|
||||
headCtx.putImageData(headImageData, 0, 0)
|
||||
|
||||
outputCtx.drawImage(headCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||
|
||||
const hatImageData = sourceCtx.getImageData(40, 8, 8, 8)
|
||||
|
||||
const hatCanvas = document.createElement('canvas')
|
||||
const hatCtx = hatCanvas.getContext('2d')
|
||||
|
||||
if (!hatCtx) {
|
||||
throw new Error('Could not get 2D context from hat canvas')
|
||||
}
|
||||
|
||||
hatCanvas.width = 8
|
||||
hatCanvas.height = 8
|
||||
hatCtx.putImageData(hatImageData, 0, 0)
|
||||
|
||||
const hatPixels = hatImageData.data
|
||||
let hasHat = false
|
||||
|
||||
for (let i = 3; i < hatPixels.length; i += 4) {
|
||||
if (hatPixels[i] > 0) {
|
||||
hasHat = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHat) {
|
||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||
}
|
||||
|
||||
outputCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
}, 'image/png')
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Failed to load skin texture image'))
|
||||
}
|
||||
|
||||
img.src = skinUrl
|
||||
})
|
||||
}
|
||||
|
||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
|
||||
if (headMap.has(headKey)) {
|
||||
if (DEBUG_MODE) {
|
||||
const url = headMap.get(headKey)!
|
||||
URL.revokeObjectURL(url)
|
||||
headMap.delete(headKey)
|
||||
} else {
|
||||
return headMap.get(headKey)!
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await skinPreviewStorage.retrieve(headKey)
|
||||
if (cached && typeof cached === 'string') {
|
||||
headMap.set(headKey, cached)
|
||||
return cached
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to retrieve cached head render:', error)
|
||||
}
|
||||
|
||||
const skinUrl = await get_normalized_skin_texture(skin)
|
||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||
const headUrl = URL.createObjectURL(headBlob)
|
||||
|
||||
headMap.set(headKey, headUrl)
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.store(headKey, headUrl)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store head render in persistent storage:', error)
|
||||
}
|
||||
|
||||
return headUrl
|
||||
}
|
||||
|
||||
export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
||||
return await generateHeadRender(skin)
|
||||
}
|
||||
|
||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
const renderer = new BatchSkinRenderer()
|
||||
|
||||
try {
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
|
||||
if (map.has(key)) {
|
||||
if (DEBUG_MODE) {
|
||||
const result = map.get(key)!
|
||||
URL.revokeObjectURL(result.forwards)
|
||||
URL.revokeObjectURL(result.backwards)
|
||||
map.delete(key)
|
||||
} else continue
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await skinPreviewStorage.retrieve(key)
|
||||
if (cached) {
|
||||
map.set(key, cached)
|
||||
continue
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to retrieve cached skin preview:', error)
|
||||
}
|
||||
|
||||
let variant = skin.variant
|
||||
if (variant === 'UNKNOWN') {
|
||||
try {
|
||||
variant = await determineModelType(skin.texture)
|
||||
} catch (error) {
|
||||
console.error(`Failed to determine model type for skin ${key}:`, error)
|
||||
variant = 'CLASSIC'
|
||||
}
|
||||
}
|
||||
|
||||
const modelUrl = getModelUrlForVariant(variant)
|
||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
||||
const renderResult = await renderer.renderSkin(
|
||||
await get_normalized_skin_texture(skin),
|
||||
modelUrl,
|
||||
cape?.texture,
|
||||
capeModelUrl,
|
||||
)
|
||||
|
||||
map.set(key, renderResult)
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.store(key, renderResult)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store skin preview in persistent storage:', error)
|
||||
}
|
||||
|
||||
await generateHeadRender(skin)
|
||||
}
|
||||
} finally {
|
||||
renderer.dispose()
|
||||
await cleanupUnusedPreviews(skins)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ export type AppSettings = {
|
||||
theme: ColorTheme
|
||||
default_page: 'home' | 'library'
|
||||
collapsed_navigation: boolean
|
||||
hide_nametag_skins_page: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
toggle_sidebar: boolean
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
|
||||
export interface Cape {
|
||||
id: string
|
||||
name: string
|
||||
texture: string
|
||||
is_default: boolean
|
||||
is_equipped: boolean
|
||||
}
|
||||
|
||||
export type SkinModel = 'CLASSIC' | 'SLIM' | 'UNKNOWN'
|
||||
export type SkinSource = 'default' | 'custom_external' | 'custom'
|
||||
|
||||
export interface Skin {
|
||||
texture_key: string
|
||||
name?: string
|
||||
variant: SkinModel
|
||||
cape_id?: string
|
||||
texture: string
|
||||
source: SkinSource
|
||||
is_equipped: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_MODEL_SORTING = ['Steve', 'Alex'] as string[]
|
||||
|
||||
export const DEFAULT_MODELS: Record<string, SkinModel> = {
|
||||
Steve: 'CLASSIC',
|
||||
Alex: 'SLIM',
|
||||
Zuri: 'CLASSIC',
|
||||
Sunny: 'CLASSIC',
|
||||
Noor: 'SLIM',
|
||||
Makena: 'SLIM',
|
||||
Kai: 'CLASSIC',
|
||||
Efe: 'SLIM',
|
||||
Ari: 'CLASSIC',
|
||||
}
|
||||
|
||||
export function filterSavedSkins(list: Skin[]) {
|
||||
const customSkins = list.filter((s) => s.source !== 'default')
|
||||
fixUnknownSkins(customSkins).catch(handleError)
|
||||
return customSkins
|
||||
}
|
||||
|
||||
export async function determineModelType(texture: string): Promise<'SLIM' | 'CLASSIC'> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
if (!context) {
|
||||
return reject(new Error('Failed to create canvas rendering context.'))
|
||||
}
|
||||
|
||||
const image = new Image()
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.src = texture
|
||||
|
||||
image.onload = () => {
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
|
||||
context.drawImage(image, 0, 0)
|
||||
|
||||
const armX = 44
|
||||
const armY = 16
|
||||
const armWidth = 4
|
||||
const armHeight = 12
|
||||
|
||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||
|
||||
for (let y = 0; y < armHeight; y++) {
|
||||
const alphaIndex = (3 + y * armWidth) * 4 + 3
|
||||
if (imageData[alphaIndex] !== 0) {
|
||||
resolve('CLASSIC')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
canvas.remove()
|
||||
resolve('SLIM')
|
||||
}
|
||||
|
||||
image.onerror = () => {
|
||||
canvas.remove()
|
||||
reject(new Error('Failed to load the image.'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function fixUnknownSkins(list: Skin[]) {
|
||||
const unknownSkins = list.filter((s) => s.variant === 'UNKNOWN')
|
||||
for (const unknownSkin of unknownSkins) {
|
||||
unknownSkin.variant = await determineModelType(unknownSkin.texture)
|
||||
}
|
||||
}
|
||||
|
||||
export function filterDefaultSkins(list: Skin[]) {
|
||||
return list
|
||||
.filter((s) => s.source === 'default' && (!s.name || s.variant === DEFAULT_MODELS[s.name]))
|
||||
.sort((a, b) => {
|
||||
const aIndex = a.name ? DEFAULT_MODEL_SORTING.indexOf(a.name) : -1
|
||||
const bIndex = b.name ? DEFAULT_MODEL_SORTING.indexOf(b.name) : -1
|
||||
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex)
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_available_capes(): Promise<Cape[]> {
|
||||
return invoke('plugin:minecraft-skins|get_available_capes', {})
|
||||
}
|
||||
|
||||
export async function get_available_skins(): Promise<Skin[]> {
|
||||
return invoke('plugin:minecraft-skins|get_available_skins', {})
|
||||
}
|
||||
|
||||
export async function add_and_equip_custom_skin(
|
||||
textureBlob: Uint8Array,
|
||||
variant: SkinModel,
|
||||
capeOverride?: Cape,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
|
||||
textureBlob,
|
||||
variant,
|
||||
capeOverride,
|
||||
})
|
||||
}
|
||||
|
||||
export async function set_default_cape(cape?: Cape): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|set_default_cape', {
|
||||
cape,
|
||||
})
|
||||
}
|
||||
|
||||
export async function equip_skin(skin: Skin): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|equip_skin', {
|
||||
skin,
|
||||
})
|
||||
}
|
||||
|
||||
export async function remove_custom_skin(skin: Skin): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|remove_custom_skin', {
|
||||
skin,
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
|
||||
const data = await normalize_skin_texture(skin.texture)
|
||||
const base64 = arrayBufferToBase64(data)
|
||||
return `data:image/png;base64,${base64}`
|
||||
}
|
||||
|
||||
export async function normalize_skin_texture(texture: Uint8Array | string): Promise<Uint8Array> {
|
||||
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { texture })
|
||||
}
|
||||
|
||||
export async function unequip_skin(): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|unequip_skin')
|
||||
}
|
||||
|
||||
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
|
||||
const data = await invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
|
||||
return new Uint8Array(data)
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import type { RenderResult } from '../rendering/batch-skin-renderer'
|
||||
|
||||
interface StoredPreview {
|
||||
forwards: Blob
|
||||
backwards: Blob
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export class SkinPreviewStorage {
|
||||
private dbName = 'skin-previews'
|
||||
private version = 1
|
||||
private db: IDBDatabase | null = null
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version)
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
resolve()
|
||||
}
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains('previews')) {
|
||||
db.createObjectStore('previews')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async store(key: string, result: RenderResult): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const forwardsBlob = await fetch(result.forwards).then((r) => r.blob())
|
||||
const backwardsBlob = await fetch(result.backwards).then((r) => r.blob())
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
const storedPreview: StoredPreview = {
|
||||
forwards: forwardsBlob,
|
||||
backwards: backwardsBlob,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.put(storedPreview, key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieve(key: string): Promise<RenderResult | null> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredPreview | undefined
|
||||
|
||||
if (!result) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
const forwards = URL.createObjectURL(result.forwards)
|
||||
const backwards = URL.createObjectURL(result.backwards)
|
||||
resolve({ forwards, backwards })
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||
const store = transaction.objectStore('previews')
|
||||
let deletedCount = 0
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
|
||||
if (!validKeys.has(key)) {
|
||||
const deleteRequest = cursor.delete()
|
||||
deleteRequest.onsuccess = () => {
|
||||
deletedCount++
|
||||
}
|
||||
deleteRequest.onerror = () => {
|
||||
console.warn('Failed to delete invalid entry:', key)
|
||||
}
|
||||
}
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
resolve(deletedCount)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const skinPreviewStorage = new SkinPreviewStorage()
|
||||
@@ -220,7 +220,6 @@ async function refreshSearch() {
|
||||
}
|
||||
}
|
||||
results.value = rawResults.result
|
||||
currentPage.value = Math.min(pageCount.value, currentPage.value)
|
||||
|
||||
const persistentParams: LocationQuery = {}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import dayjs from 'dayjs'
|
||||
import { get_search_results } from '@/helpers/cache.js'
|
||||
import type { SearchResult } from '@modrinth/utils'
|
||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
@@ -83,15 +82,13 @@ async function refreshFeaturedProjects() {
|
||||
await fetchInstances()
|
||||
await refreshFeaturedProjects()
|
||||
|
||||
const unlistenProfile = await profile_listener(
|
||||
async (e: { event: string; profile_path_id: string }) => {
|
||||
await fetchInstances()
|
||||
const unlistenProfile = await profile_listener(async (e) => {
|
||||
await fetchInstances()
|
||||
|
||||
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
|
||||
await refreshFeaturedProjects()
|
||||
}
|
||||
},
|
||||
)
|
||||
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
|
||||
await refreshFeaturedProjects()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
@@ -100,8 +97,8 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="p-6 flex flex-col gap-2">
|
||||
<h1 v-if="recentInstances?.length > 0" class="m-0 text-2xl font-extrabold">Welcome back!</h1>
|
||||
<h1 v-else class="m-0 text-2xl font-extrabold">Welcome to Modrinth App!</h1>
|
||||
<h1 v-if="recentInstances" class="m-0 text-2xl">Welcome back!</h1>
|
||||
<h1 v-else class="m-0 text-2xl">Welcome to Modrinth App!</h1>
|
||||
<RecentWorldsList :recent-instances="recentInstances" />
|
||||
<RowDisplay
|
||||
v-if="hasFeaturedProjects"
|
||||
|
||||
@@ -1,528 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EditIcon,
|
||||
ExcitedRinthbot,
|
||||
LogInIcon,
|
||||
PlusIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
UpdatedIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
SkinButton,
|
||||
SkinLikeTextButton,
|
||||
SkinPreviewRenderer,
|
||||
} from '@modrinth/ui'
|
||||
import { computedAsync } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
|
||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||
import { handleError, useNotifications } from '@/store/notifications'
|
||||
import type { Cape, Skin } from '@/helpers/skins.ts'
|
||||
import {
|
||||
normalize_skin_texture,
|
||||
equip_skin,
|
||||
filterDefaultSkins,
|
||||
filterSavedSkins,
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
get_normalized_skin_texture,
|
||||
remove_custom_skin,
|
||||
set_default_cape,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, map } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
|
||||
const editSkinModal = useTemplateRef('editSkinModal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
const uploadSkinModal = useTemplateRef('uploadSkinModal')
|
||||
|
||||
const notifications = useNotifications()
|
||||
|
||||
const settings = ref(await getSettings())
|
||||
const skins = ref<Skin[]>([])
|
||||
const capes = ref<Cape[]>([])
|
||||
|
||||
const accountsCard = inject('accountsCard') as Ref<typeof AccountsCard>
|
||||
const currentUser = ref(undefined)
|
||||
const currentUserId = ref<string | undefined>(undefined)
|
||||
|
||||
const username = computed(() => currentUser.value?.profile?.name ?? undefined)
|
||||
const selectedSkin = ref<Skin | null>(null)
|
||||
const defaultCape = ref<Cape>()
|
||||
|
||||
const originalSelectedSkin = ref<Skin | null>(null)
|
||||
const originalDefaultCape = ref<Cape>()
|
||||
|
||||
const savedSkins = computed(() => filterSavedSkins(skins.value))
|
||||
const defaultSkins = computed(() => filterDefaultSkins(skins.value))
|
||||
|
||||
const currentCape = computed(() => {
|
||||
if (selectedSkin.value?.cape_id) {
|
||||
const overrideCape = capes.value.find((c) => c.id === selectedSkin.value?.cape_id)
|
||||
if (overrideCape) {
|
||||
return overrideCape
|
||||
}
|
||||
}
|
||||
return defaultCape.value
|
||||
})
|
||||
|
||||
const skinTexture = computedAsync(async () => {
|
||||
if (selectedSkin.value?.texture) {
|
||||
return await get_normalized_skin_texture(selectedSkin.value)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const capeTexture = computed(() => currentCape.value?.texture)
|
||||
const skinVariant = computed(() => selectedSkin.value?.variant)
|
||||
const skinNametag = computed(() =>
|
||||
settings.value.hide_nametag_skins_page ? undefined : username.value,
|
||||
)
|
||||
|
||||
let userCheckInterval: number | null = null
|
||||
|
||||
const deleteSkinModal = ref()
|
||||
const skinToDelete = ref<Skin | null>(null)
|
||||
|
||||
function confirmDeleteSkin(skin: Skin) {
|
||||
skinToDelete.value = skin
|
||||
deleteSkinModal.value?.show()
|
||||
}
|
||||
|
||||
async function deleteSkin() {
|
||||
if (!skinToDelete.value) return
|
||||
await remove_custom_skin(skinToDelete.value).catch(handleError)
|
||||
await loadSkins()
|
||||
skinToDelete.value = null
|
||||
}
|
||||
|
||||
async function loadCapes() {
|
||||
try {
|
||||
capes.value = (await get_available_capes()) ?? []
|
||||
defaultCape.value = capes.value.find((c) => c.is_equipped)
|
||||
originalDefaultCape.value = defaultCape.value
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkins() {
|
||||
try {
|
||||
skins.value = (await get_available_skins()) ?? []
|
||||
generateSkinPreviews(skins.value, capes.value)
|
||||
selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
|
||||
originalSelectedSkin.value = selectedSkin.value
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function changeSkin(newSkin: Skin) {
|
||||
const previousSkin = selectedSkin.value
|
||||
const previousSkinsList = [...skins.value]
|
||||
|
||||
skins.value = skins.value.map((skin) => {
|
||||
return {
|
||||
...skin,
|
||||
is_equipped: skin.texture_key === newSkin.texture_key,
|
||||
}
|
||||
})
|
||||
|
||||
selectedSkin.value = skins.value.find((s) => s.texture_key === newSkin.texture_key) || null
|
||||
|
||||
try {
|
||||
await equip_skin(newSkin)
|
||||
if (accountsCard.value) {
|
||||
await accountsCard.value.refreshValues()
|
||||
}
|
||||
} catch (error) {
|
||||
selectedSkin.value = previousSkin
|
||||
skins.value = previousSkinsList
|
||||
|
||||
if ((error as { message?: string })?.message?.includes('429 Too Many Requests')) {
|
||||
notifications.addNotification({
|
||||
type: 'error',
|
||||
title: 'Slow down!',
|
||||
text: "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
||||
})
|
||||
} else {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCapeSelected(cape: Cape | undefined) {
|
||||
const previousDefaultCape = defaultCape.value
|
||||
const previousCapesList = [...capes.value]
|
||||
|
||||
capes.value = capes.value.map((c) => ({
|
||||
...c,
|
||||
is_equipped: cape ? c.id === cape.id : false,
|
||||
}))
|
||||
|
||||
defaultCape.value = cape ? capes.value.find((c) => c.id === cape.id) : undefined
|
||||
|
||||
try {
|
||||
await set_default_cape(cape)
|
||||
} catch (error) {
|
||||
defaultCape.value = previousDefaultCape
|
||||
capes.value = previousCapesList
|
||||
|
||||
if ((error as { message?: string })?.message?.includes('429 Too Many Requests')) {
|
||||
notifications.addNotification({
|
||||
type: 'error',
|
||||
title: 'Slow down!',
|
||||
text: "You're changing your cape too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
||||
})
|
||||
} else {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onSkinSaved() {
|
||||
await Promise.all([loadCapes(), loadSkins()])
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
try {
|
||||
const defaultId = await get_default_user()
|
||||
currentUserId.value = defaultId
|
||||
|
||||
const allAccounts = await users()
|
||||
currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId)
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
currentUser.value = undefined
|
||||
currentUserId.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
return map.get(key)
|
||||
}
|
||||
|
||||
async function login() {
|
||||
accountsCard.value.setLoginDisabled(true)
|
||||
const loggedIn = await login_flow().catch(handleSevereError)
|
||||
|
||||
if (loggedIn && accountsCard) {
|
||||
await accountsCard.value.refreshValues()
|
||||
}
|
||||
|
||||
trackEvent('AccountLogIn')
|
||||
accountsCard.value.setLoginDisabled(false)
|
||||
}
|
||||
|
||||
function openUploadSkinModal(e: MouseEvent) {
|
||||
uploadSkinModal.value?.show(e)
|
||||
}
|
||||
|
||||
function onSkinFileUploaded(buffer: ArrayBuffer) {
|
||||
const fakeEvent = new MouseEvent('click')
|
||||
normalize_skin_texture(`data:image/png;base64,` + arrayBufferToBase64(buffer)).then(
|
||||
(skinTextureNormalized: Uint8Array) => {
|
||||
const skinTexUrl = `data:image/png;base64,` + arrayBufferToBase64(skinTextureNormalized)
|
||||
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
|
||||
editSkinModal.value.restoreWithNewTexture(skinTexUrl)
|
||||
} else {
|
||||
editSkinModal.value?.showNew(fakeEvent, skinTexUrl)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function onUploadCanceled() {
|
||||
editSkinModal.value?.restoreModal()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedSkin.value?.cape_id,
|
||||
() => {},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
userCheckInterval = window.setInterval(checkUserChanges, 250)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (userCheckInterval !== null) {
|
||||
window.clearInterval(userCheckInterval)
|
||||
}
|
||||
})
|
||||
|
||||
async function checkUserChanges() {
|
||||
try {
|
||||
const defaultId = await get_default_user()
|
||||
if (defaultId !== currentUserId.value) {
|
||||
await loadCurrentUser()
|
||||
await loadCapes()
|
||||
await loadSkins()
|
||||
}
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EditSkinModal
|
||||
ref="editSkinModal"
|
||||
:capes="capes"
|
||||
:default-cape="defaultCape"
|
||||
@saved="onSkinSaved"
|
||||
@deleted="() => loadSkins()"
|
||||
@open-upload-modal="openUploadSkinModal"
|
||||
/>
|
||||
<SelectCapeModal ref="selectCapeModal" :capes="capes" @select="handleCapeSelected" />
|
||||
<UploadSkinModal
|
||||
ref="uploadSkinModal"
|
||||
@uploaded="onSkinFileUploaded"
|
||||
@canceled="onUploadCanceled"
|
||||
/>
|
||||
<ConfirmModal
|
||||
ref="deleteSkinModal"
|
||||
title="Are you sure you want to delete this skin?"
|
||||
description="This will permanently delete the selected skin. This action cannot be undone."
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteSkin"
|
||||
/>
|
||||
|
||||
<div v-if="currentUser" class="p-4 skin-layout">
|
||||
<div class="preview-panel">
|
||||
<h1 class="m-0 text-2xl font-bold flex items-center gap-2">
|
||||
Skins
|
||||
<span class="text-sm font-bold px-2 bg-brand-highlight text-brand rounded-full">Beta</span>
|
||||
</h1>
|
||||
<div class="preview-container">
|
||||
<SkinPreviewRenderer
|
||||
:wide-model-src="wideModelUrl"
|
||||
:slim-model-src="slimModelUrl"
|
||||
:cape-model-src="capeModelUrl"
|
||||
:cape-src="capeTexture"
|
||||
:texture-src="skinTexture || ''"
|
||||
:variant="skinVariant"
|
||||
:nametag="skinNametag"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
>
|
||||
<template #subtitle>
|
||||
<ButtonStyled :disabled="!!selectedSkin?.cape_id">
|
||||
<button
|
||||
v-tooltip="
|
||||
selectedSkin?.cape_id
|
||||
? 'The equipped skin is overriding the default cape.'
|
||||
: undefined
|
||||
"
|
||||
:disabled="!!selectedSkin?.cape_id"
|
||||
@click="
|
||||
(e: MouseEvent) =>
|
||||
selectCapeModal?.show(
|
||||
e,
|
||||
selectedSkin?.texture_key,
|
||||
currentCape,
|
||||
skinTexture,
|
||||
skinVariant,
|
||||
)
|
||||
"
|
||||
>
|
||||
<UpdatedIcon />
|
||||
Change cape
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</SkinPreviewRenderer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skins-container">
|
||||
<section class="flex flex-col gap-2 mt-1">
|
||||
<h2 class="text-lg font-bold m-0 text-primary">Saved skins</h2>
|
||||
<div class="skin-card-grid">
|
||||
<SkinLikeTextButton class="skin-card" @click="openUploadSkinModal">
|
||||
<template #icon>
|
||||
<PlusIcon class="size-8" />
|
||||
</template>
|
||||
<span>Add a skin</span>
|
||||
</SkinLikeTextButton>
|
||||
|
||||
<SkinButton
|
||||
v-for="skin in savedSkins"
|
||||
:key="`saved-skin-${skin.texture_key}`"
|
||||
class="skin-card"
|
||||
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
|
||||
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
|
||||
:selected="selectedSkin === skin"
|
||||
@select="changeSkin(skin)"
|
||||
>
|
||||
<template #overlay-buttons>
|
||||
<Button
|
||||
color="green"
|
||||
aria-label="Edit skin"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(e) => editSkinModal?.show(e, skin)"
|
||||
>
|
||||
<EditIcon /> Edit
|
||||
</Button>
|
||||
<Button
|
||||
v-show="!skin.is_equipped"
|
||||
v-tooltip="'Delete skin'"
|
||||
aria-label="Delete skin"
|
||||
color="red"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
icon-only
|
||||
@click.stop="() => confirmDeleteSkin(skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-2 mt-6">
|
||||
<h2 class="text-lg font-bold m-0 text-primary">Default skins</h2>
|
||||
<div class="skin-card-grid">
|
||||
<SkinButton
|
||||
v-for="skin in defaultSkins"
|
||||
:key="`default-skin-${skin.texture_key}`"
|
||||
class="skin-card"
|
||||
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
|
||||
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
|
||||
:selected="selectedSkin === skin"
|
||||
:tooltip="skin.name"
|
||||
@select="changeSkin(skin)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex items-center justify-center min-h-[50vh] pt-[25%]">
|
||||
<div
|
||||
class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
|
||||
>
|
||||
<img
|
||||
:src="ExcitedRinthbot"
|
||||
alt="Excited Modrinth Bot"
|
||||
class="absolute -top-28 right-8 md:right-20 h-28 w-auto"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-[1px] opacity-40 bg-gradient-to-r from-transparent via-green-500 to-transparent"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 2rem,
|
||||
var(--color-green) calc(100% - 13rem),
|
||||
var(--color-green) calc(100% - 5rem),
|
||||
transparent calc(100% - 2rem)
|
||||
);
|
||||
"
|
||||
></div>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
<h1 class="text-3xl font-extrabold m-0">Please sign-in</h1>
|
||||
<p class="text-lg m-0">
|
||||
Please sign into your Minecraft account to use the skin management features of the
|
||||
Modrinth app.
|
||||
</p>
|
||||
<ButtonStyled v-show="accountsCard" color="brand" :disabled="accountsCard.loginDisabled">
|
||||
<button :disabled="accountsCard.loginDisabled" @click="login">
|
||||
<LogInIcon v-if="!accountsCard.loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
Sign In
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$skin-card-width: 155px;
|
||||
$skin-card-gap: 4px;
|
||||
|
||||
.skin-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr);
|
||||
gap: 2.5rem;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
top: 1.5rem;
|
||||
position: sticky;
|
||||
align-self: start;
|
||||
padding: 0.5rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: calc((2.5rem / 2));
|
||||
|
||||
@media (max-width: 700px) {
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
.skins-container {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.skin-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $skin-card-gap;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 1300px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 1750px) {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 2050px) {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.skin-card {
|
||||
aspect-ratio: 0.95;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
import Index from './Index.vue'
|
||||
import Browse from './Browse.vue'
|
||||
import Worlds from './Worlds.vue'
|
||||
import Skins from './Skins.vue'
|
||||
|
||||
export { Index, Browse, Worlds, Skins }
|
||||
export { Index, Browse, Worlds }
|
||||
@@ -34,14 +34,6 @@ export default new createRouter({
|
||||
breadcrumb: [{ name: 'Discover content' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/skins',
|
||||
name: 'Skins',
|
||||
component: Pages.Skins,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Skins' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/library',
|
||||
name: 'Library',
|
||||
|
||||
@@ -41,7 +41,6 @@ export default {
|
||||
green: 'var(--color-green-highlight)',
|
||||
blue: 'var(--color-blue-highlight)',
|
||||
purple: 'var(--color-purple-highlight)',
|
||||
gray: 'var(--color-gray-highlight)',
|
||||
},
|
||||
divider: {
|
||||
DEFAULT: 'var(--color-divider)',
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"strict": true
|
||||
},
|
||||
|
||||
@@ -4,8 +4,6 @@ import svgLoader from 'vite-svg-loader'
|
||||
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
import tauriConf from '../app/tauri.conf.json'
|
||||
|
||||
const projectRootDir = resolve(__dirname)
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@@ -43,32 +41,17 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
headers: {
|
||||
'content-security-policy': Object.entries(tauriConf.app.security.csp)
|
||||
.map(([directive, sources]) => {
|
||||
// An additional websocket connect-src is required for Vite dev tools to work
|
||||
if (directive === 'connect-src') {
|
||||
sources = Array.isArray(sources) ? sources : [sources]
|
||||
sources.push('ws://localhost:1420')
|
||||
}
|
||||
|
||||
return Array.isArray(sources)
|
||||
? `${directive} ${sources.join(' ')}`
|
||||
: `${directive} ${sources}`
|
||||
})
|
||||
.join('; '),
|
||||
},
|
||||
},
|
||||
// to make use of `TAURI_ENV_DEBUG` and other env variables
|
||||
// https://v2.tauri.app/reference/environment-variables/#tauri-cli-hook-commands
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: process.env.TAURI_ENV_PLATFORM == 'windows' ? 'chrome105' : 'safari13', // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13', // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_ENV_DEBUG, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
sourcemap: !!process.env.TAURI_DEBUG, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
commonjsOptions: {
|
||||
esmExternals: true,
|
||||
},
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by tauri, metadata generated at compile time
|
||||
/gen/
|
||||
+8
-9
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.10.0"
|
||||
version = "0.9.5"
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
@@ -16,15 +16,14 @@ serde_json.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_with.workspace = true
|
||||
|
||||
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset"] }
|
||||
tauri-plugin-deep-link.workspace = true
|
||||
tauri-plugin-dialog.workspace = true
|
||||
tauri-plugin-http.workspace = true
|
||||
tauri-plugin-opener.workspace = true
|
||||
tauri-plugin-os.workspace = true
|
||||
tauri-plugin-single-instance.workspace = true
|
||||
tauri-plugin-updater.workspace = true
|
||||
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||
tauri-plugin-window-state.workspace = true
|
||||
tauri-plugin-deep-link.workspace = true
|
||||
tauri-plugin-os.workspace = true
|
||||
tauri-plugin-opener.workspace = true
|
||||
tauri-plugin-dialog.workspace = true
|
||||
tauri-plugin-updater.workspace = true
|
||||
tauri-plugin-single-instance.workspace = true
|
||||
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -99,24 +99,6 @@ fn main() {
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"minecraft-skins",
|
||||
InlinedPlugin::new()
|
||||
.commands(&[
|
||||
"get_available_capes",
|
||||
"get_available_skins",
|
||||
"add_and_equip_custom_skin",
|
||||
"set_default_cape",
|
||||
"equip_skin",
|
||||
"remove_custom_skin",
|
||||
"unequip_skin",
|
||||
"normalize_skin_texture",
|
||||
"get_dragged_skin_data",
|
||||
])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"mr-auth",
|
||||
InlinedPlugin::new()
|
||||
|
||||
@@ -19,21 +19,12 @@
|
||||
"window-state:default",
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state",
|
||||
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "https://modrinth.com/*" },
|
||||
{ "url": "https://*.modrinth.com/*" }
|
||||
]
|
||||
},
|
||||
|
||||
"auth:default",
|
||||
"import:default",
|
||||
"jre:default",
|
||||
"logs:default",
|
||||
"metadata:default",
|
||||
"minecraft-skins:default",
|
||||
"mr-auth:default",
|
||||
"profile-create:default",
|
||||
"pack:default",
|
||||
|
||||
@@ -41,8 +41,8 @@ pub async fn jre_find_filtered_jres(
|
||||
// Validates JRE at a given path
|
||||
// Returns None if the path is not a valid JRE
|
||||
#[tauri::command]
|
||||
pub async fn jre_get_jre(path: PathBuf) -> Result<JavaVersion> {
|
||||
Ok(jre::check_jre(path).await?)
|
||||
pub async fn jre_get_jre(path: PathBuf) -> Result<Option<JavaVersion>> {
|
||||
jre::check_jre(path).await.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
// Tests JRE of a certain version
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
use crate::api::Result;
|
||||
|
||||
use std::path::Path;
|
||||
use theseus::minecraft_skins::{
|
||||
self, Bytes, Cape, MinecraftSkinVariant, Skin, UrlOrBlob,
|
||||
};
|
||||
|
||||
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("minecraft-skins")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
add_and_equip_custom_skin,
|
||||
set_default_cape,
|
||||
equip_skin,
|
||||
remove_custom_skin,
|
||||
unequip_skin,
|
||||
normalize_skin_texture,
|
||||
get_dragged_skin_data,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_available_capes')`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_available_capes]
|
||||
#[tauri::command]
|
||||
pub async fn get_available_capes() -> Result<Vec<Cape>> {
|
||||
Ok(minecraft_skins::get_available_capes().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_available_skins')`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_available_skins]
|
||||
#[tauri::command]
|
||||
pub async fn get_available_skins() -> Result<Vec<Skin>> {
|
||||
Ok(minecraft_skins::get_available_skins().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape_override)`
|
||||
///
|
||||
/// See also: [minecraft_skins::add_and_equip_custom_skin]
|
||||
#[tauri::command]
|
||||
pub async fn add_and_equip_custom_skin(
|
||||
texture_blob: Bytes,
|
||||
variant: MinecraftSkinVariant,
|
||||
cape_override: Option<Cape>,
|
||||
) -> Result<()> {
|
||||
Ok(minecraft_skins::add_and_equip_custom_skin(
|
||||
texture_blob,
|
||||
variant,
|
||||
cape_override,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|set_default_cape', cape)`
|
||||
///
|
||||
/// See also: [minecraft_skins::set_default_cape]
|
||||
#[tauri::command]
|
||||
pub async fn set_default_cape(cape: Option<Cape>) -> Result<()> {
|
||||
Ok(minecraft_skins::set_default_cape(cape).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|equip_skin', skin)`
|
||||
///
|
||||
/// See also: [minecraft_skins::equip_skin]
|
||||
#[tauri::command]
|
||||
pub async fn equip_skin(skin: Skin) -> Result<()> {
|
||||
Ok(minecraft_skins::equip_skin(skin).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|remove_custom_skin', skin)`
|
||||
///
|
||||
/// See also: [minecraft_skins::remove_custom_skin]
|
||||
#[tauri::command]
|
||||
pub async fn remove_custom_skin(skin: Skin) -> Result<()> {
|
||||
Ok(minecraft_skins::remove_custom_skin(skin).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|unequip_skin')`
|
||||
///
|
||||
/// See also: [minecraft_skins::unequip_skin]
|
||||
#[tauri::command]
|
||||
pub async fn unequip_skin() -> Result<()> {
|
||||
Ok(minecraft_skins::unequip_skin().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|normalize_skin_texture')`
|
||||
///
|
||||
/// See also: [minecraft_skins::normalize_skin_texture]
|
||||
#[tauri::command]
|
||||
pub async fn normalize_skin_texture(texture: UrlOrBlob) -> Result<Bytes> {
|
||||
Ok(minecraft_skins::normalize_skin_texture(&texture).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_dragged_skin_data', path)`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_dragged_skin_data]
|
||||
#[tauri::command]
|
||||
pub async fn get_dragged_skin_data(path: String) -> Result<Bytes> {
|
||||
let path = Path::new(&path);
|
||||
Ok(minecraft_skins::get_dragged_skin_data(path).await?)
|
||||
}
|
||||
@@ -7,7 +7,6 @@ pub mod import;
|
||||
pub mod jre;
|
||||
pub mod logs;
|
||||
pub mod metadata;
|
||||
pub mod minecraft_skins;
|
||||
pub mod mr_auth;
|
||||
pub mod pack;
|
||||
pub mod process;
|
||||
|
||||
@@ -183,7 +183,6 @@ fn main() {
|
||||
let _ = win.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
@@ -249,7 +248,6 @@ fn main() {
|
||||
.plugin(api::logs::init())
|
||||
.plugin(api::jre::init())
|
||||
.plugin(api::metadata::init())
|
||||
.plugin(api::minecraft_skins::init())
|
||||
.plugin(api::pack::init())
|
||||
.plugin(api::process::init())
|
||||
.plugin(api::profile::init())
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
{
|
||||
"bundle": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
"build": {
|
||||
"features": ["updater"]
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"externalBin": [],
|
||||
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.digicert.com",
|
||||
"nsis": {
|
||||
"installMode": "perMachine",
|
||||
"installerHooks": "./nsis/hooks.nsi"
|
||||
@@ -27,6 +30,7 @@
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"linux": {
|
||||
"deb": {
|
||||
@@ -41,7 +45,7 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.10.0",
|
||||
"version": "0.9.5",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
@@ -86,9 +90,9 @@
|
||||
"capabilities": ["ads", "core", "plugins"],
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"script-src": "https://*.posthog.com 'self'",
|
||||
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.88.0 AS build
|
||||
FROM rust:1.87.0 AS build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/daedalus
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
Support: https://support.modrinth.com
|
||||
Status page: https://status.modrinth.com
|
||||
Roadmap: https://roadmap.modrinth.com
|
||||
Blog and newsletter: https://modrinth.com/news
|
||||
Blog and newsletter: https://blog.modrinth.com/subscribe?utm_medium=social&utm_source=discord&utm_campaign=welcome
|
||||
API documentation: https://docs.modrinth.com
|
||||
Modrinth source code: https://github.com/modrinth
|
||||
Help translate Modrinth: https://crowdin.com/project/modrinth
|
||||
|
||||
@@ -85,10 +85,11 @@ During development, you might notice that changes made directly to entities in t
|
||||
|
||||
#### CDN options
|
||||
|
||||
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local` or `s3`, but defaults to `local`
|
||||
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local`, `backblaze`, or `s3`, but defaults to `local`
|
||||
|
||||
The S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
||||
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_PUBLIC_BUCKET_NAME`, `S3_PRIVATE_BUCKET_NAME`, `S3_USES_PATH_STYLE_BUCKETS`
|
||||
The Backblaze and 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_BUCKET_NAME`
|
||||
|
||||
#### Search, OAuth, and miscellaneous options
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ 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
|
||||
PROD_OVERRIDE=true
|
||||
|
||||
@@ -40,9 +40,7 @@
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@modrinth/blog": "workspace:*",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"ace-builds": "^1.36.2",
|
||||
@@ -61,6 +59,7 @@
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"three": "^0.172.0",
|
||||
"@types/three": "^0.172.0",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-typed-virtual-list": "^1.0.10",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
<template>
|
||||
<div class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised">
|
||||
<nuxt-link
|
||||
to="/servers"
|
||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
|
||||
>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-light.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="light-image hidden rounded-[inherit]"
|
||||
/>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-dark.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="dark-image rounded-[inherit]"
|
||||
/>
|
||||
</nuxt-link>
|
||||
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
|
||||
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
||||
<nuxt-link to="/plus" class="mt-auto items-center gap-1 text-purple hover:underline">
|
||||
<span>
|
||||
Support creators and Modrinth ad-free with
|
||||
<span class="font-bold">Modrinth+</span>
|
||||
</span>
|
||||
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 flex items-center justify-center overflow-hidden rounded-2xl bg-bg-raised"
|
||||
>
|
||||
@@ -23,6 +18,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ChevronRightIcon } from "@modrinth/assets";
|
||||
|
||||
useHead({
|
||||
script: [
|
||||
// {
|
||||
@@ -140,16 +137,3 @@ iframe[id^="google_ads_iframe"] {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.light,
|
||||
.light-mode {
|
||||
.dark-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.light-image {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<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>
|
||||
@@ -0,0 +1,131 @@
|
||||
<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>
|
||||
@@ -0,0 +1,75 @@
|
||||
<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>
|
||||
@@ -9,7 +9,7 @@
|
||||
</h1>
|
||||
<ButtonStyled circular color="red" color-fill="none" hover-color-fill="background">
|
||||
<button v-tooltip="`Exit moderation`" @click="exitModeration">
|
||||
<XIcon />
|
||||
<CrossIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
@@ -306,7 +306,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled v-if="!done">
|
||||
<button aria-label="Skip" @click="goToNextProject">
|
||||
<XIcon aria-hidden="true" />
|
||||
<ExitIcon aria-hidden="true" />
|
||||
<template v-if="futureProjects.length > 0">Skip</template>
|
||||
<template v-else>Exit</template>
|
||||
</button>
|
||||
@@ -335,7 +335,7 @@
|
||||
<div class="joined-buttons">
|
||||
<ButtonStyled color="red">
|
||||
<button @click="sendMessage('rejected')">
|
||||
<XIcon aria-hidden="true" /> Reject
|
||||
<CrossIcon aria-hidden="true" /> Reject
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
@@ -373,8 +373,9 @@ import {
|
||||
UpdatedIcon,
|
||||
CheckIcon,
|
||||
DropdownIcon,
|
||||
XIcon as CrossIcon,
|
||||
EyeOffIcon,
|
||||
XIcon,
|
||||
ExitIcon,
|
||||
ScaleIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, MarkdownEditor, OverflowMenu, Collapsible } from "@modrinth/ui";
|
||||
@@ -653,11 +654,11 @@ For a brief rundown of how this works:
|
||||
{
|
||||
name: "Insufficient",
|
||||
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).
|
||||
Keep in mind that you should:
|
||||
- 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.
|
||||
- Upload any relevant images in your Description to your Gallery tab for best results.`,
|
||||
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:
|
||||
- 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.
|
||||
- Upload any relevant images in your Description to your Gallery tab for best results.`,
|
||||
},
|
||||
{
|
||||
name: "Not relevant",
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { MailIcon, CheckIcon } from "@modrinth/assets";
|
||||
import { ref, watchEffect } from "vue";
|
||||
import { useBaseFetch } from "~/composables/fetch.js";
|
||||
|
||||
const auth = await useAuth();
|
||||
const showSubscriptionConfirmation = ref(false);
|
||||
const subscribed = ref(false);
|
||||
|
||||
async function checkSubscribed() {
|
||||
if (auth.value?.user) {
|
||||
try {
|
||||
const { data } = await useBaseFetch("auth/email/subscribe", {
|
||||
method: "GET",
|
||||
});
|
||||
subscribed.value = data?.subscribed || false;
|
||||
} catch {
|
||||
subscribed.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
checkSubscribed();
|
||||
});
|
||||
|
||||
async function subscribe() {
|
||||
try {
|
||||
await useBaseFetch("auth/email/subscribe", {
|
||||
method: "POST",
|
||||
});
|
||||
showSubscriptionConfirmation.value = true;
|
||||
} catch {
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
showSubscriptionConfirmation.value = false;
|
||||
subscribed.value = true;
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ButtonStyled v-if="auth?.user && !subscribed" color="brand" type="outlined">
|
||||
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
|
||||
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
|
||||
<template v-else> <CheckIcon /> Subscribed! </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -104,13 +104,13 @@
|
||||
</nuxt-link>
|
||||
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
||||
has been
|
||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||
<Badge :type="notification.body.new_status" />
|
||||
</template>
|
||||
<template v-else>
|
||||
updated from
|
||||
<ProjectStatusBadge :status="notification.body.old_status" />
|
||||
<Badge :type="notification.body.old_status" />
|
||||
to
|
||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||
<Badge :type="notification.body.new_status" />
|
||||
</template>
|
||||
by the moderators.
|
||||
</template>
|
||||
@@ -331,13 +331,16 @@ import {
|
||||
XIcon,
|
||||
ExternalIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
||||
import { useRelativeTime } from "@modrinth/ui";
|
||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
||||
import { getUserLink } from "~/helpers/users.js";
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
||||
import { markAsRead } from "~/helpers/notifications.ts";
|
||||
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";
|
||||
|
||||
const app = useNuxtApp();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="vue-notification-group experimental-styles-within"
|
||||
:class="{ 'intercom-present': isIntercomPresent }"
|
||||
>
|
||||
<div class="vue-notification-group experimental-styles-within">
|
||||
<transition-group name="notifs">
|
||||
<div
|
||||
v-for="(item, index) in notifications"
|
||||
@@ -83,8 +80,6 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
const notifications = useNotifications();
|
||||
|
||||
const isIntercomPresent = ref(false);
|
||||
|
||||
function stopTimer(notif) {
|
||||
clearTimeout(notif.timer);
|
||||
}
|
||||
@@ -111,27 +106,6 @@ const createNotifText = (notif) => {
|
||||
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) {
|
||||
const text = createNotifText(notif);
|
||||
|
||||
@@ -156,10 +130,6 @@ function copyToClipboard(notif) {
|
||||
bottom: 0.75rem;
|
||||
}
|
||||
|
||||
&.intercom-present {
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
.vue-notification-wrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<div class="table-cell">
|
||||
<BoxIcon />
|
||||
<span>{{
|
||||
formatProjectType(
|
||||
$formatProjectType(
|
||||
$getProjectTypeForDisplay(
|
||||
project.project_types?.[0] ?? "project",
|
||||
project.loaders,
|
||||
@@ -111,7 +111,6 @@
|
||||
<script setup>
|
||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
||||
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
|
||||
import { formatProjectType } from "@modrinth/utils";
|
||||
|
||||
const modalOpen = ref(null);
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<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 }}
|
||||
</nuxt-link>
|
||||
</p>
|
||||
<ProjectStatusBadge v-if="status && status !== 'approved'" :status="status" class="status" />
|
||||
<Badge v-if="status && status !== 'approved'" :type="status" class="status" />
|
||||
</div>
|
||||
<p class="description">
|
||||
{{ description }}
|
||||
@@ -91,16 +91,18 @@
|
||||
|
||||
<script>
|
||||
import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets";
|
||||
import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import { useRelativeTime } from "@modrinth/ui";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProjectStatusBadge,
|
||||
EnvironmentIndicator,
|
||||
Avatar,
|
||||
Categories,
|
||||
Badge,
|
||||
CalendarIcon,
|
||||
UpdatedIcon,
|
||||
DownloadIcon,
|
||||
|
||||
@@ -118,7 +118,7 @@ import {
|
||||
ScaleIcon,
|
||||
DropdownIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { formatProjectType } from "@modrinth/utils";
|
||||
import { formatProjectType } from "~/plugins/shorthands.js";
|
||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share on Bluesky`"
|
||||
:href="`https://bsky.app/intent/compose?text=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<BlueskyIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share on Mastodon`"
|
||||
:href="`https://tootpick.org/#text=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<MastodonIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share on X`"
|
||||
:href="`https://www.x.com/intent/post?url=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="`Share via email`"
|
||||
:href="`mailto:${encodedTitle ? `?subject=${encodedTitle}&` : `?`}body=${encodedUrl}`"
|
||||
target="_blank"
|
||||
>
|
||||
<MailIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="copied ? `Copied to clipboard` : `Copy link`"
|
||||
:disabled="copied"
|
||||
class="relative grid place-items-center overflow-hidden"
|
||||
@click="copyToClipboard(url)"
|
||||
>
|
||||
<CheckIcon
|
||||
class="absolute transition-all ease-in-out"
|
||||
:class="copied ? 'translate-y-0' : 'translate-y-7'"
|
||||
/>
|
||||
<LinkIcon
|
||||
class="absolute transition-all ease-in-out"
|
||||
:class="copied ? '-translate-y-7' : 'translate-y-0'"
|
||||
/>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BlueskyIcon,
|
||||
CheckIcon,
|
||||
LinkIcon,
|
||||
MailIcon,
|
||||
MastodonIcon,
|
||||
TwitterIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const copied = ref(false);
|
||||
const encodedUrl = computed(() => encodeURIComponent(props.url));
|
||||
const encodedTitle = computed(() => (props.title ? encodeURIComponent(props.title) : undefined));
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<div class="mx-2 p-4 !py-8 sm:mx-8 sm:p-32">
|
||||
<div class="my-8 flex items-center justify-between">
|
||||
<h2 class="m-0 mx-auto text-3xl font-extrabold sm:text-4xl">Latest news from Modrinth</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="latestArticles" class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
|
||||
<div
|
||||
v-for="(article, index) in latestArticles"
|
||||
:key="article.slug"
|
||||
:class="{ 'max-xl:hidden': index === 2 }"
|
||||
>
|
||||
<NewsArticleCard :article="article" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-2 my-8 flex w-full items-center justify-center">
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<nuxt-link to="/news">
|
||||
<NewspaperIcon />
|
||||
View all news
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NewspaperIcon } from "@modrinth/assets";
|
||||
import { articles as rawArticles } from "@modrinth/blog";
|
||||
import { ButtonStyled, NewsArticleCard } from "@modrinth/ui";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const articles = ref(
|
||||
rawArticles
|
||||
.map((article) => ({
|
||||
...article,
|
||||
path: `/news/article/${article.slug}/`,
|
||||
thumbnail: article.thumbnail
|
||||
? `/news/article/${article.slug}/thumbnail.webp`
|
||||
: `/news/default.webp`,
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
date: article.date,
|
||||
}))
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
||||
);
|
||||
|
||||
const latestArticles = computed(() => articles.value.slice(0, 3));
|
||||
</script>
|
||||
@@ -11,8 +11,8 @@
|
||||
<div class="stacked">
|
||||
<span class="title">{{ report.project.title }}</span>
|
||||
<span>{{
|
||||
formatProjectType(
|
||||
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
$formatProjectType(
|
||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -53,8 +53,8 @@
|
||||
<div class="stacked">
|
||||
<span class="title">{{ report.project.title }}</span>
|
||||
<span>{{
|
||||
formatProjectType(
|
||||
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
$formatProjectType(
|
||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -104,11 +104,12 @@
|
||||
|
||||
<script setup>
|
||||
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 { 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 { getProjectTypeForUrl } from "~/helpers/projects.js";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
<span
|
||||
v-for="category in categoriesFiltered"
|
||||
:key="category.name"
|
||||
v-html="category.icon + formatCategory(category.name)"
|
||||
v-html="category.icon + $formatCategory(category.name)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatCategory } from "@modrinth/utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
categories: {
|
||||
@@ -40,7 +38,6 @@ export default {
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: { formatCategory },
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||
import { ref, computed } from "vue";
|
||||
import { ref } from "vue";
|
||||
import type { Backup } from "@modrinth/utils";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
@@ -52,10 +52,9 @@ const failedToCreate = computed(() => props.backup.interrupted);
|
||||
const preparedDownloadStates = ["ready", "done"];
|
||||
const inactiveStates = ["failed", "cancelled"];
|
||||
|
||||
const hasPreparedDownload = computed(() => {
|
||||
const fileState = props.backup.task?.file?.state ?? "";
|
||||
return preparedDownloadStates.includes(fileState);
|
||||
});
|
||||
const hasPreparedDownload = computed(() =>
|
||||
preparedDownloadStates.includes(props.backup.task?.file?.state ?? ""),
|
||||
);
|
||||
|
||||
const creating = computed(() => {
|
||||
const task = props.backup.task?.create;
|
||||
@@ -82,10 +81,6 @@ const restoring = computed(() => {
|
||||
const initiatedPrepare = ref(false);
|
||||
|
||||
const preparingFile = computed(() => {
|
||||
if (hasPreparedDownload.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const task = props.backup.task?.file;
|
||||
return (
|
||||
(!task && initiatedPrepare.value) ||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<NewModal ref="modModal" :header="`Editing ${type.toLocaleLowerCase()} version`">
|
||||
<template #title>
|
||||
<div class="flex min-w-full items-center gap-2 md:w-[calc(420px-5.5rem)]">
|
||||
<Avatar :src="modDetails?.icon_url" size="48px" :alt="`${modDetails?.name} Icon`" />
|
||||
<UiAvatar :src="modDetails?.icon_url" size="48px" :alt="`${modDetails?.name} Icon`" />
|
||||
<span class="truncate text-xl font-extrabold text-contrast">{{ modDetails?.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -185,7 +185,7 @@
|
||||
Something went wrong trying to load versions for this {{ type.toLocaleLowerCase() }}.
|
||||
Please try again later or contact support if the issue persists.
|
||||
</span>
|
||||
<CopyCode class="!mt-2 !break-all" :text="versionsError" />
|
||||
<LazyUiCopyCode class="!mt-2 !break-all" :text="versionsError" />
|
||||
</div>
|
||||
</Admonition>
|
||||
|
||||
@@ -236,7 +236,7 @@ import {
|
||||
GameIcon,
|
||||
ExternalIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from "@modrinth/ui";
|
||||
import { Admonition, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils";
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
/>
|
||||
<XCircleIcon
|
||||
v-show="
|
||||
item.status.includes('error') ||
|
||||
item.status === 'error' ||
|
||||
item.status === 'cancelled' ||
|
||||
item.status === 'incorrect-type'
|
||||
"
|
||||
@@ -54,14 +54,9 @@
|
||||
<template v-if="item.status === 'completed'">
|
||||
<span>Done</span>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'error-file-exists'">
|
||||
<template v-else-if="item.status === 'error'">
|
||||
<span class="text-red">Failed - File already exists</span>
|
||||
</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'">
|
||||
<span class="text-red">Failed - Incorrect file type</span>
|
||||
</template>
|
||||
@@ -109,17 +104,9 @@ import { FSModule } from "~/composables/servers/modules/fs.ts";
|
||||
interface UploadItem {
|
||||
file: File;
|
||||
progress: number;
|
||||
status:
|
||||
| "pending"
|
||||
| "uploading"
|
||||
| "completed"
|
||||
| "error-file-exists"
|
||||
| "error-generic"
|
||||
| "cancelled"
|
||||
| "incorrect-type";
|
||||
status: "pending" | "uploading" | "completed" | "error" | "cancelled" | "incorrect-type";
|
||||
size: string;
|
||||
uploader?: any;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -258,18 +245,8 @@ const uploadFile = async (file: File) => {
|
||||
console.error("Error uploading file:", error);
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||
const target = uploadQueue.value[index];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
uploadQueue.value[index].status =
|
||||
error instanceof Error && error.message === badFileTypeMsg ? "incorrect-type" : "error";
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
|
||||
@@ -1,176 +1,124 @@
|
||||
<template>
|
||||
<NewModal ref="mrpackModal" header="Uploading mrpack" :closable="!isLoading" @show="onShow">
|
||||
<NewModal ref="mrpackModal" header="Uploading mrpack" @hide="onHide" @show="onShow">
|
||||
<div class="flex flex-col gap-4 md:w-[600px]">
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-20"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 max-h-20"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
<p
|
||||
v-if="isMrpackModalSecondPhase"
|
||||
:style="{
|
||||
lineHeight: isMrpackModalSecondPhase ? '1.5' : undefined,
|
||||
marginBottom: isMrpackModalSecondPhase ? '-12px' : '0',
|
||||
marginTop: isMrpackModalSecondPhase ? '-4px' : '-2px',
|
||||
}"
|
||||
>
|
||||
<div v-if="isLoading" class="w-full">
|
||||
<div class="mb-2 flex justify-between text-sm">
|
||||
<Transition name="phrase-fade" mode="out-in">
|
||||
<span :key="currentPhrase" class="text-lg font-medium text-contrast">{{
|
||||
currentPhrase
|
||||
}}</span>
|
||||
</Transition>
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="text-secondary">{{ Math.round(uploadProgress) }}%</span>
|
||||
<span class="text-xs text-secondary"
|
||||
>{{ formatBytes(uploadedBytes) }} / {{ formatBytes(totalBytes) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-2 w-full rounded-full bg-divider">
|
||||
<div
|
||||
class="h-2 animate-pulse rounded-full bg-brand transition-all duration-300 ease-out"
|
||||
:style="{ width: `${uploadProgress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-20"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 max-h-20"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
>
|
||||
<div v-if="!isLoading" class="flex flex-col gap-4">
|
||||
<p
|
||||
v-if="isMrpackModalSecondPhase"
|
||||
:style="{
|
||||
lineHeight: isMrpackModalSecondPhase ? '1.5' : undefined,
|
||||
marginBottom: isMrpackModalSecondPhase ? '-12px' : '0',
|
||||
marginTop: isMrpackModalSecondPhase ? '-4px' : '-2px',
|
||||
}"
|
||||
This will reinstall your server and erase all data. You may want to back up your server
|
||||
before proceeding. Are you sure you want to continue?
|
||||
</p>
|
||||
<div v-if="!isMrpackModalSecondPhase" class="flex flex-col gap-4">
|
||||
<div class="mx-auto flex flex-row items-center gap-4">
|
||||
<div
|
||||
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
>
|
||||
This will reinstall your server and erase all data. You may want to back up your server
|
||||
before proceeding. Are you sure you want to continue?
|
||||
</p>
|
||||
<div v-if="!isMrpackModalSecondPhase" class="flex flex-col gap-4">
|
||||
<div class="mx-auto flex flex-row items-center gap-4">
|
||||
<div
|
||||
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
>
|
||||
<UploadIcon class="size-10" />
|
||||
</div>
|
||||
<ArrowBigRightDashIcon class="size-10" />
|
||||
<div
|
||||
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-table-alternateRow shadow-sm"
|
||||
>
|
||||
<ServerIcon class="size-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="text-sm font-bold text-contrast">Upload mrpack</div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".mrpack"
|
||||
class=""
|
||||
:disabled="isLoading"
|
||||
@change="uploadMrpack"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="flex w-full flex-row items-center justify-between">
|
||||
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
|
||||
Erase all data
|
||||
</label>
|
||||
<input
|
||||
id="hard-reset"
|
||||
v-model="hardReset"
|
||||
class="switch stylized-toggle shrink-0"
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Removes all data on your server, including your worlds, mods, and configuration
|
||||
files, then reinstalls it with the selected version.
|
||||
</div>
|
||||
<div class="font-bold">
|
||||
This does not affect your backups, which are stored off-site.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackupWarning :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
||||
<UploadIcon class="size-10" />
|
||||
</div>
|
||||
<div class="mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled :color="isDangerous ? 'red' : 'brand'">
|
||||
<button
|
||||
v-tooltip="backupInProgress ? backupInProgress.tooltip : undefined"
|
||||
:disabled="canInstall || !!backupInProgress"
|
||||
@click="handleReinstall"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
{{
|
||||
isMrpackModalSecondPhase
|
||||
? "Erase and install"
|
||||
: loadingServerCheck
|
||||
? "Loading..."
|
||||
: isDangerous
|
||||
? "Erase and install"
|
||||
: "Install"
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
:disabled="isLoading"
|
||||
@click="
|
||||
() => {
|
||||
if (isMrpackModalSecondPhase) {
|
||||
isMrpackModalSecondPhase = false;
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
{{ isMrpackModalSecondPhase ? "Go back" : "Cancel" }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="size-10"
|
||||
>
|
||||
<path d="M5 9v6" />
|
||||
<path d="M9 9h3V5l7 7-7 7v-4H9V9z" />
|
||||
</svg>
|
||||
<div
|
||||
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-table-alternateRow shadow-sm"
|
||||
>
|
||||
<ServerIcon class="size-10" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="text-sm font-bold text-contrast">Upload mrpack</div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".mrpack"
|
||||
class=""
|
||||
:disabled="isLoading"
|
||||
@change="uploadMrpack"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="flex w-full flex-row items-center justify-between">
|
||||
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
|
||||
Erase all data
|
||||
</label>
|
||||
<input
|
||||
id="hard-reset"
|
||||
v-model="hardReset"
|
||||
class="switch stylized-toggle shrink-0"
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Removes all data on your server, including your worlds, mods, and configuration files,
|
||||
then reinstalls it with the selected version.
|
||||
</div>
|
||||
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
||||
</div>
|
||||
|
||||
<BackupWarning :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
||||
</div>
|
||||
<div class="mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled :color="isDangerous ? 'red' : 'brand'">
|
||||
<button
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:disabled="canInstall || backupInProgress"
|
||||
@click="handleReinstall"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
{{
|
||||
isMrpackModalSecondPhase
|
||||
? "Erase and install"
|
||||
: loadingServerCheck
|
||||
? "Loading..."
|
||||
: isDangerous
|
||||
? "Erase and install"
|
||||
: "Install"
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
:disabled="isLoading"
|
||||
@click="
|
||||
() => {
|
||||
if (isMrpackModalSecondPhase) {
|
||||
isMrpackModalSecondPhase = false;
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
{{ isMrpackModalSecondPhase ? "Go back" : "Cancel" }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import {
|
||||
UploadIcon,
|
||||
RightArrowIcon,
|
||||
XIcon,
|
||||
ServerIcon,
|
||||
ArrowBigRightDashIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
import { UploadIcon, RightArrowIcon, XIcon, ServerIcon } from "@modrinth/assets";
|
||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import type { ModrinthServer } from "~/composables/servers/modrinth-servers";
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (isLoading.value) {
|
||||
event.preventDefault();
|
||||
return "Upload in progress. Are you sure you want to leave?";
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
});
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: ModrinthServer;
|
||||
@@ -187,49 +135,6 @@ const hardReset = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const loadingServerCheck = ref(false);
|
||||
const mrpackFile = ref<File | null>(null);
|
||||
const uploadProgress = ref(0);
|
||||
const uploadedBytes = ref(0);
|
||||
const totalBytes = ref(0);
|
||||
|
||||
const uploadPhrases = [
|
||||
"Removing Herobrine...",
|
||||
"Feeding parrots...",
|
||||
"Teaching villagers new trades...",
|
||||
"Convincing creepers to be friendly...",
|
||||
"Polishing diamonds...",
|
||||
"Training wolves to fetch...",
|
||||
"Building pixel art...",
|
||||
"Explaining redstone to beginners...",
|
||||
"Collecting all the cats...",
|
||||
"Negotiating with endermen...",
|
||||
"Planting suspicious stew ingredients...",
|
||||
"Calibrating TNT blast radius...",
|
||||
"Teaching chickens to fly...",
|
||||
"Sorting inventory alphabetically...",
|
||||
"Convincing iron golems to smile...",
|
||||
];
|
||||
|
||||
const currentPhrase = ref("Uploading...");
|
||||
let phraseInterval: NodeJS.Timeout | null = null;
|
||||
const usedPhrases = ref(new Set<number>());
|
||||
|
||||
const getNextPhrase = () => {
|
||||
if (usedPhrases.value.size >= uploadPhrases.length) {
|
||||
const currentPhraseIndex = uploadPhrases.indexOf(currentPhrase.value);
|
||||
usedPhrases.value.clear();
|
||||
if (currentPhraseIndex !== -1) {
|
||||
usedPhrases.value.add(currentPhraseIndex);
|
||||
}
|
||||
}
|
||||
const availableIndices = uploadPhrases
|
||||
.map((_, index) => index)
|
||||
.filter((index) => !usedPhrases.value.has(index));
|
||||
|
||||
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)];
|
||||
usedPhrases.value.add(randomIndex);
|
||||
|
||||
return uploadPhrases[randomIndex];
|
||||
};
|
||||
|
||||
const isDangerous = computed(() => hardReset.value);
|
||||
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value);
|
||||
@@ -248,46 +153,18 @@ const handleReinstall = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mrpackFile.value) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "No file selected",
|
||||
text: "Choose a .mrpack file before installing.",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
uploadProgress.value = 0;
|
||||
uploadedBytes.value = 0;
|
||||
totalBytes.value = mrpackFile.value.size;
|
||||
|
||||
currentPhrase.value = getNextPhrase();
|
||||
phraseInterval = setInterval(() => {
|
||||
currentPhrase.value = getNextPhrase();
|
||||
}, 4500);
|
||||
|
||||
const { onProgress, promise } = props.server.general.reinstallFromMrpack(
|
||||
mrpackFile.value,
|
||||
hardReset.value,
|
||||
);
|
||||
|
||||
onProgress(({ loaded, total, progress }) => {
|
||||
uploadProgress.value = progress;
|
||||
uploadedBytes.value = loaded;
|
||||
totalBytes.value = total;
|
||||
|
||||
if (phraseInterval && progress >= 100) {
|
||||
clearInterval(phraseInterval);
|
||||
phraseInterval = null;
|
||||
currentPhrase.value = "Installing modpack...";
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await promise;
|
||||
if (!mrpackFile.value) {
|
||||
throw new Error("No mrpack file selected");
|
||||
}
|
||||
|
||||
const mrpack = new File([mrpackFile.value], mrpackFile.value.name, {
|
||||
type: mrpackFile.value.type,
|
||||
});
|
||||
|
||||
await props.server.general?.reinstallFromMrpack(mrpack, hardReset.value);
|
||||
|
||||
emit("reinstall", {
|
||||
loader: "mrpack",
|
||||
@@ -299,44 +176,36 @@ const handleReinstall = async () => {
|
||||
window.scrollTo(0, 0);
|
||||
hide();
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Cannot upload and install modpack to server",
|
||||
title: "Cannot reinstall server",
|
||||
text: "You are being rate limited. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
} else {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Modpack upload and install failed",
|
||||
text: "An unexpected error occurred while uploading/installing. Please try again later.",
|
||||
title: "Reinstall Failed",
|
||||
text: "An unexpected error occurred while reinstalling. Please try again later.",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
if (phraseInterval) {
|
||||
clearInterval(phraseInterval);
|
||||
phraseInterval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onShow = () => {
|
||||
hardReset.value = false;
|
||||
isMrpackModalSecondPhase.value = false;
|
||||
loadingServerCheck.value = false;
|
||||
isLoading.value = false;
|
||||
mrpackFile.value = null;
|
||||
uploadProgress.value = 0;
|
||||
uploadedBytes.value = 0;
|
||||
totalBytes.value = 0;
|
||||
currentPhrase.value = "Uploading...";
|
||||
usedPhrases.value.clear();
|
||||
if (phraseInterval) {
|
||||
clearInterval(phraseInterval);
|
||||
phraseInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onHide = () => {
|
||||
onShow();
|
||||
};
|
||||
|
||||
const show = () => mrpackModal.value?.show();
|
||||
@@ -349,14 +218,4 @@ defineExpose({ show, hide });
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
|
||||
.phrase-fade-enter-active,
|
||||
.phrase-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.phrase-fade-enter-from,
|
||||
.phrase-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -33,7 +33,7 @@
|
||||
v-if="projectData?.title"
|
||||
class="m-0 flex flex-row items-center gap-2 text-sm font-medium text-secondary"
|
||||
>
|
||||
<Avatar
|
||||
<UiAvatar
|
||||
:src="iconUrl"
|
||||
no-shadow
|
||||
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
|
||||
@@ -74,7 +74,7 @@
|
||||
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been suspended. Please
|
||||
update your billing information or contact Modrinth Support for more information.
|
||||
</div>
|
||||
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
<UiCopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
@@ -83,7 +83,6 @@
|
||||
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
|
||||
import type { Project, Server } from "@modrinth/utils";
|
||||
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||
import { Avatar, CopyCode } from "@modrinth/ui";
|
||||
|
||||
const props = defineProps<Partial<Server>>();
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
<nuxt-link
|
||||
|
||||
<component
|
||||
:is="loading ? 'div' : 'NuxtLink'"
|
||||
:to="loading ? undefined : `/servers/manage/${serverId}/files`"
|
||||
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||
:class="loading ? '' : 'transition-transform duration-100 hover:scale-105 active:scale-100'"
|
||||
@@ -62,17 +64,16 @@
|
||||
</div>
|
||||
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
||||
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
||||
</nuxt-link>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, shallowRef } from "vue";
|
||||
import { FolderOpenIcon, CpuIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import { FolderOpenIcon, CPUIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import type { Stats } from "@modrinth/utils";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const route = useNativeRoute();
|
||||
const serverId = route.params.id;
|
||||
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
||||
@@ -126,7 +127,7 @@ const metrics = computed(() => {
|
||||
title: "CPU usage",
|
||||
value: "0.00%",
|
||||
max: "100%",
|
||||
icon: CpuIcon,
|
||||
icon: CPUIcon,
|
||||
data: cpuData.value,
|
||||
showGraph: false,
|
||||
warning: null,
|
||||
@@ -157,21 +158,17 @@ const metrics = computed(() => {
|
||||
title: "CPU usage",
|
||||
value: `${cpuPercent.toFixed(2)}%`,
|
||||
max: "100%",
|
||||
icon: CpuIcon,
|
||||
icon: CPUIcon,
|
||||
data: cpuData.value,
|
||||
showGraph: true,
|
||||
warning: cpuPercent >= 90 ? "CPU usage is very high" : null,
|
||||
},
|
||||
{
|
||||
title: "Memory usage",
|
||||
value:
|
||||
userPreferences.value.ramAsNumber || flags.developerMode
|
||||
? formatBytes(stats.value.ram_usage_bytes)
|
||||
: `${ramPercent.toFixed(2)}%`,
|
||||
max:
|
||||
userPreferences.value.ramAsNumber || flags.developerMode
|
||||
? formatBytes(stats.value.ram_total_bytes)
|
||||
: "100%",
|
||||
value: userPreferences.value.ramAsNumber
|
||||
? formatBytes(stats.value.ram_usage_bytes)
|
||||
: `${ramPercent.toFixed(2)}%`,
|
||||
max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%",
|
||||
icon: DatabaseIcon,
|
||||
data: ramData.value,
|
||||
showGraph: true,
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { CopyCode, OverflowMenu, MarkdownEditor } from "@modrinth/ui";
|
||||
import { OverflowMenu, MarkdownEditor } from "@modrinth/ui";
|
||||
import {
|
||||
DropdownIcon,
|
||||
ReplyIcon,
|
||||
@@ -226,6 +226,7 @@ import {
|
||||
ScaleIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { useImageUpload } from "~/composables/image-upload.ts";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
||||
import { isStaff } from "~/helpers/users.js";
|
||||
import { isApproved, isRejected } from "~/helpers/projects.js";
|
||||
|
||||
@@ -103,8 +103,10 @@ import {
|
||||
ModrinthIcon,
|
||||
ScaleIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
||||
import { AutoLink, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
||||
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";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
const formatters = new WeakMap<object, Intl.NumberFormat>();
|
||||
import { createFormatter, type Formatter } from "@vintl/compact-number";
|
||||
import type { IntlController } from "@vintl/vintl/controller";
|
||||
|
||||
export function useCompactNumber(truncate = false, fractionDigits = 2, locale?: string) {
|
||||
const context = {};
|
||||
const formatters = new WeakMap<IntlController<any>, Formatter>();
|
||||
|
||||
let formatter = formatters.get(context);
|
||||
export function useCompactNumber(): Formatter {
|
||||
const vintl = useVIntl();
|
||||
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(locale, {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: fractionDigits,
|
||||
});
|
||||
formatters.set(context, formatter);
|
||||
let formatter = formatters.get(vintl);
|
||||
|
||||
if (formatter == null) {
|
||||
const formatterRef = computed(() => createFormatter(vintl.intl));
|
||||
formatter = (value, options) => formatterRef.value(value, options);
|
||||
formatters.set(vintl, formatter);
|
||||
}
|
||||
|
||||
function format(value: number): string {
|
||||
let formattedValue = value;
|
||||
if (truncate) {
|
||||
const scale = Math.pow(10, fractionDigits);
|
||||
formattedValue = Math.floor(value * scale) / scale;
|
||||
}
|
||||
return formatter!.format(formattedValue);
|
||||
}
|
||||
|
||||
return format;
|
||||
return formatter;
|
||||
}
|
||||
@@ -31,9 +31,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
projectBackground: false,
|
||||
searchBackground: false,
|
||||
advancedDebugInfo: false,
|
||||
showProjectPageDownloadModalServersPromo: false,
|
||||
showProjectPageCreateServersTooltip: true,
|
||||
showProjectPageQuickServerButton: false,
|
||||
// advancedRendering: true,
|
||||
// externalLinksNewTab: true,
|
||||
// notUsingBlockers: false,
|
||||
|
||||
@@ -124,63 +124,58 @@ export class ModrinthServer {
|
||||
return dataURL;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError && error.statusCode === 404) {
|
||||
if (iconUrl) {
|
||||
try {
|
||||
const response = await fetch(iconUrl);
|
||||
if (!response.ok) throw new Error("Failed to fetch icon");
|
||||
const file = await response.blob();
|
||||
const originalFile = new File([file], "server-icon-original.png", {
|
||||
type: "image/png",
|
||||
});
|
||||
if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) {
|
||||
// Handle external icon processing
|
||||
try {
|
||||
const response = await fetch(iconUrl);
|
||||
if (!response.ok) throw new Error("Failed to fetch icon");
|
||||
const file = await response.blob();
|
||||
const originalFile = new File([file], "server-icon-original.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;
|
||||
}
|
||||
} catch (externalError: any) {
|
||||
console.debug("Could not process external icon:", externalError.message);
|
||||
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 (error) {
|
||||
console.error("Failed to process external icon:", error);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.debug("Icon processing failed:", error.message);
|
||||
} catch (error) {
|
||||
console.error("Failed to process server icon:", error);
|
||||
}
|
||||
|
||||
sharedImage.value = undefined;
|
||||
@@ -244,18 +239,6 @@ export class ModrinthServer {
|
||||
break;
|
||||
}
|
||||
} 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] = {
|
||||
error:
|
||||
error instanceof ModrinthServerError
|
||||
|
||||
@@ -98,67 +98,28 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
}
|
||||
}
|
||||
|
||||
reinstallFromMrpack(
|
||||
mrpack: File,
|
||||
hardReset: boolean = false,
|
||||
): {
|
||||
promise: Promise<void>;
|
||||
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void;
|
||||
} {
|
||||
async reinstallFromMrpack(mrpack: File, hardReset: boolean = false): Promise<void> {
|
||||
const hardResetParam = hardReset ? "true" : "false";
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`);
|
||||
|
||||
const progressSubject = new EventTarget();
|
||||
const formData = new FormData();
|
||||
formData.append("file", mrpack);
|
||||
|
||||
const uploadPromise = (async () => {
|
||||
try {
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`);
|
||||
const response = await fetch(
|
||||
`https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.token}`,
|
||||
},
|
||||
body: formData,
|
||||
signal: AbortSignal.timeout(30 * 60 * 1000),
|
||||
},
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable) {
|
||||
progressSubject.dispatchEvent(
|
||||
new CustomEvent("progress", {
|
||||
detail: {
|
||||
loaded: e.loaded,
|
||||
total: e.total,
|
||||
progress: (e.loaded / e.total) * 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onload = () =>
|
||||
xhr.status >= 200 && xhr.status < 300
|
||||
? resolve()
|
||||
: reject(new Error(`[pyroservers] XHR error status: ${xhr.status}`));
|
||||
|
||||
xhr.onerror = () => reject(new Error("[pyroservers] .mrpack upload failed"));
|
||||
xhr.onabort = () => reject(new Error("[pyroservers] .mrpack upload cancelled"));
|
||||
xhr.ontimeout = () => reject(new Error("[pyroservers] .mrpack upload timed out"));
|
||||
xhr.timeout = 30 * 60 * 1000;
|
||||
|
||||
xhr.open("POST", `https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`);
|
||||
xhr.setRequestHeader("Authorization", `Bearer ${auth.token}`);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", mrpack);
|
||||
xhr.send(formData);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error reinstalling from mrpack:", err);
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
promise: uploadPromise,
|
||||
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) =>
|
||||
progressSubject.addEventListener("progress", ((e: CustomEvent) =>
|
||||
cb(e.detail)) as EventListener),
|
||||
};
|
||||
if (!response.ok) {
|
||||
throw new Error(`[pyroservers] native fetch err status: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async suspend(status: boolean): Promise<void> {
|
||||
@@ -194,25 +155,19 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
}
|
||||
|
||||
async setMotd(motd: string): Promise<void> {
|
||||
try {
|
||||
const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
|
||||
if (props) {
|
||||
props.motd = motd;
|
||||
const newProps = this.server.constructServerProperties(props);
|
||||
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
||||
const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
|
||||
if (props) {
|
||||
props.motd = motd;
|
||||
const newProps = this.server.constructServerProperties(props);
|
||||
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
||||
|
||||
await useServersFetch(`/update?path=/server.properties`, {
|
||||
method: "PUT",
|
||||
contentType: "application/octet-stream",
|
||||
body: octetStream,
|
||||
override: auth,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.error(
|
||||
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
|
||||
);
|
||||
await useServersFetch(`/update?path=/server.properties`, {
|
||||
method: "PUT",
|
||||
contentType: "application/octet-stream",
|
||||
body: octetStream,
|
||||
override: auth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatBytes } from "@modrinth/utils";
|
||||
import { formatBytes } from "~/plugins/shorthands.js";
|
||||
|
||||
export const fileIsValid = (file, validationOptions) => {
|
||||
const { maxSize, alertOnInvalid } = validationOptions;
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
"
|
||||
>
|
||||
<nuxt-link to="/resourcepacks">
|
||||
<PaintbrushIcon aria-hidden="true" /> Resource Packs
|
||||
<PaintBrushIcon aria-hidden="true" /> Resource Packs
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
@@ -221,7 +221,7 @@
|
||||
v-if="route.name === 'search-mods' || route.path.startsWith('/mod/')"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<PaintbrushIcon
|
||||
<PaintBrushIcon
|
||||
v-else-if="
|
||||
route.name === 'search-resourcepacks' || route.path.startsWith('/resourcepack/')
|
||||
"
|
||||
@@ -250,7 +250,7 @@
|
||||
|
||||
<template #mods> <BoxIcon aria-hidden="true" /> Mods </template>
|
||||
<template #resourcepacks>
|
||||
<PaintbrushIcon aria-hidden="true" /> Resource Packs
|
||||
<PaintBrushIcon aria-hidden="true" /> Resource Packs
|
||||
</template>
|
||||
<template #datapacks> <BracesIcon aria-hidden="true" /> Data Packs </template>
|
||||
<template #plugins> <PlugIcon aria-hidden="true" /> Plugins </template>
|
||||
@@ -696,14 +696,14 @@ import {
|
||||
CurrencyIcon,
|
||||
BracesIcon,
|
||||
GlassesIcon,
|
||||
PaintbrushIcon,
|
||||
PaintBrushIcon,
|
||||
PackageOpenIcon,
|
||||
DiscordIcon,
|
||||
BlueskyIcon,
|
||||
TumblrIcon,
|
||||
TwitterIcon,
|
||||
MastodonIcon,
|
||||
GithubIcon,
|
||||
GitHubIcon,
|
||||
ScaleIcon,
|
||||
} from "@modrinth/assets";
|
||||
import {
|
||||
@@ -1202,7 +1202,7 @@ const socialLinks = [
|
||||
defineMessage({ id: "layout.footer.social.github", defaultMessage: "GitHub" }),
|
||||
),
|
||||
href: "https://github.com/modrinth",
|
||||
icon: GithubIcon,
|
||||
icon: GitHubIcon,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1211,9 +1211,9 @@ const footerLinks = [
|
||||
label: formatMessage(defineMessage({ id: "layout.footer.about", defaultMessage: "About" })),
|
||||
links: [
|
||||
{
|
||||
href: "/news",
|
||||
href: "https://blog.modrinth.com",
|
||||
label: formatMessage(
|
||||
defineMessage({ id: "layout.footer.about.news", defaultMessage: "News" }),
|
||||
defineMessage({ id: "layout.footer.about.blog", defaultMessage: "Blog" }),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -383,8 +383,8 @@
|
||||
"layout.footer.about": {
|
||||
"message": "About"
|
||||
},
|
||||
"layout.footer.about.news": {
|
||||
"message": "News"
|
||||
"layout.footer.about.blog": {
|
||||
"message": "Blog"
|
||||
},
|
||||
"layout.footer.about.careers": {
|
||||
"message": "Careers"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<h1 class="wrap-as-needed">
|
||||
{{ project.title }}
|
||||
</h1>
|
||||
<ProjectStatusBadge :status="project.status" />
|
||||
<Badge :type="project.status" />
|
||||
</div>
|
||||
</div>
|
||||
<h2>Project settings</h2>
|
||||
@@ -452,16 +452,6 @@
|
||||
{{ formatCategory(currentPlatform) }}.
|
||||
</p>
|
||||
</AutomaticAccordion>
|
||||
<ServersPromo
|
||||
v-if="flags.showProjectPageDownloadModalServersPromo"
|
||||
:link="`/servers#plan`"
|
||||
@close="
|
||||
() => {
|
||||
flags.showProjectPageDownloadModalServersPromo = false;
|
||||
saveFeatureFlags();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -505,64 +495,6 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</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>
|
||||
<ButtonStyled
|
||||
size="large"
|
||||
@@ -762,7 +694,12 @@
|
||||
:tags="tags"
|
||||
class="card flex-card experimental-styles-within"
|
||||
/>
|
||||
<AdPlaceholder v-if="!auth.user && tags.approvedStatuses.includes(project.status)" />
|
||||
<AdPlaceholder
|
||||
v-if="
|
||||
(!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus) &&
|
||||
tags.approvedStatuses.includes(project.status)
|
||||
"
|
||||
/>
|
||||
<ProjectSidebarLinks
|
||||
:project="project"
|
||||
:link-target="$external()"
|
||||
@@ -913,14 +850,12 @@ import {
|
||||
ReportIcon,
|
||||
ScaleIcon,
|
||||
SearchIcon,
|
||||
ServerPlusIcon,
|
||||
SettingsIcon,
|
||||
TagsIcon,
|
||||
UsersIcon,
|
||||
VersionIcon,
|
||||
WrenchIcon,
|
||||
ModrinthIcon,
|
||||
XIcon,
|
||||
} from "@modrinth/assets";
|
||||
import {
|
||||
Avatar,
|
||||
@@ -935,27 +870,17 @@ import {
|
||||
ProjectSidebarCreators,
|
||||
ProjectSidebarDetails,
|
||||
ProjectSidebarLinks,
|
||||
ProjectStatusBadge,
|
||||
ScrollablePanel,
|
||||
TagItem,
|
||||
ServersPromo,
|
||||
useRelativeTime,
|
||||
} from "@modrinth/ui";
|
||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||
import {
|
||||
formatCategory,
|
||||
formatProjectType,
|
||||
isRejected,
|
||||
isStaff,
|
||||
isUnderReview,
|
||||
renderString,
|
||||
} from "@modrinth/utils";
|
||||
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
|
||||
import { navigateTo } from "#app";
|
||||
import dayjs from "dayjs";
|
||||
import { Tooltip } from "floating-vue";
|
||||
import Accordion from "~/components/ui/Accordion.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
||||
@@ -966,7 +891,6 @@ import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
|
||||
import { userCollectProject } from "~/composables/user.js";
|
||||
import { reportProject } from "~/utils/report-helpers.ts";
|
||||
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
|
||||
|
||||
const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
@@ -1362,7 +1286,7 @@ featuredVersions.value.sort((a, b) => {
|
||||
});
|
||||
|
||||
const projectTypeDisplay = computed(() =>
|
||||
formatProjectType(
|
||||
data.$formatProjectType(
|
||||
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
|
||||
),
|
||||
);
|
||||
@@ -1380,10 +1304,6 @@ const description = computed(
|
||||
} 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")) {
|
||||
useSeoMeta({
|
||||
title: () => title.value,
|
||||
@@ -1752,33 +1672,4 @@ const navLinks = computed(() => {
|
||||
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>
|
||||
@@ -50,7 +50,7 @@
|
||||
Listed in search results
|
||||
</li>
|
||||
<li v-else>
|
||||
<XIcon class="bad" />
|
||||
<ExitIcon class="bad" />
|
||||
Not listed in search results
|
||||
</li>
|
||||
<li v-if="isListed(project)">
|
||||
@@ -58,11 +58,11 @@
|
||||
Listed on the profiles of members
|
||||
</li>
|
||||
<li v-else>
|
||||
<XIcon class="bad" />
|
||||
<ExitIcon class="bad" />
|
||||
Not listed on the profiles of members
|
||||
</li>
|
||||
<li v-if="isPrivate(project)">
|
||||
<XIcon class="bad" />
|
||||
<ExitIcon class="bad" />
|
||||
Not accessible with a direct link
|
||||
</li>
|
||||
<li v-else>
|
||||
@@ -92,7 +92,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { XIcon, CheckIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import { ExitIcon, CheckIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import { Badge } from "@modrinth/ui";
|
||||
import ConversationThread from "~/components/ui/thread/ConversationThread.vue";
|
||||
import {
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<span class="label__title">Client-side</span>
|
||||
<span class="label__description">
|
||||
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 functionality.
|
||||
</span>
|
||||
@@ -128,7 +128,7 @@
|
||||
<span class="label__title">Server-side</span>
|
||||
<span class="label__description">
|
||||
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
|
||||
server.
|
||||
</span>
|
||||
@@ -239,10 +239,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
|
||||
import { formatProjectStatus } from "@modrinth/utils";
|
||||
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { ConfirmModal, Avatar } from "@modrinth/ui";
|
||||
import { ConfirmModal } from "@modrinth/ui";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import FileInput from "~/components/ui/FileInput.vue";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
{{ formatProjectType(project.project_type).toLowerCase() }}. You may choose one from our
|
||||
list or provide a custom license. You may also provide a custom URL to your chosen license;
|
||||
otherwise, the license text will be displayed. See our
|
||||
<nuxt-link
|
||||
to="/news/article/licensing-guide/"
|
||||
<a
|
||||
href="https://blog.modrinth.com/licensing-guide/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-link"
|
||||
>
|
||||
licensing guide
|
||||
</nuxt-link>
|
||||
</a>
|
||||
for more information.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<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}`">
|
||||
<div class="label">
|
||||
<h4>
|
||||
<span class="label__title">{{ formatCategoryHeader(header) }}</span>
|
||||
<span class="label__title">{{ $formatCategoryHeader(header) }}</span>
|
||||
</h4>
|
||||
<span class="label__description">
|
||||
<template v-if="header === 'categories'">
|
||||
Select all categories that reflect the themes or function of your
|
||||
{{ formatProjectType(project.project_type).toLowerCase() }}.
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
||||
</template>
|
||||
<template v-else-if="header === 'features'">
|
||||
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 v-else-if="header === 'resolutions'">
|
||||
Select the resolution(s) of textures in your
|
||||
{{ formatProjectType(project.project_type).toLowerCase() }}.
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
||||
</template>
|
||||
<template v-else-if="header === 'performance impact'">
|
||||
Select the realistic performance impact of your
|
||||
{{ formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
|
||||
{{ formatProjectType(project.project_type).toLowerCase() }} is configurable to
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} is configurable to
|
||||
different levels of performance impact.
|
||||
</template>
|
||||
</span>
|
||||
@@ -46,7 +46,7 @@
|
||||
v-for="category in categoryLists[header]"
|
||||
:key="`category-${header}-${category.name}`"
|
||||
:model-value="selectedTags.includes(category)"
|
||||
:description="formatCategory(category.name)"
|
||||
:description="$formatCategory(category.name)"
|
||||
class="category-selector"
|
||||
@update:model-value="toggleCategory(category)"
|
||||
>
|
||||
@@ -57,7 +57,7 @@
|
||||
class="icon"
|
||||
v-html="category.icon"
|
||||
/>
|
||||
<span aria-hidden="true"> {{ formatCategory(category.name) }}</span>
|
||||
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
@@ -80,7 +80,7 @@
|
||||
:key="`featured-category-${category.name}`"
|
||||
class="category-selector"
|
||||
:model-value="featuredTags.includes(category)"
|
||||
:description="formatCategory(category.name)"
|
||||
:description="$formatCategory(category.name)"
|
||||
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
|
||||
@update:model-value="toggleFeaturedCategory(category)"
|
||||
>
|
||||
@@ -91,7 +91,7 @@
|
||||
class="icon"
|
||||
v-html="category.icon"
|
||||
/>
|
||||
<span aria-hidden="true"> {{ formatCategory(category.name) }}</span>
|
||||
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
@@ -114,7 +114,6 @@
|
||||
|
||||
<script>
|
||||
import { StarIcon, SaveIcon } from "@modrinth/assets";
|
||||
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
@@ -223,9 +222,6 @@ export default defineNuxtComponent({
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatProjectType,
|
||||
formatCategoryHeader,
|
||||
formatCategory,
|
||||
toggleCategory(category) {
|
||||
if (this.selectedTags.includes(category)) {
|
||||
this.selectedTags = this.selectedTags.filter((x) => x !== category);
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
<div v-else class="input-group">
|
||||
<ButtonStyled v-if="primaryFile" color="brand">
|
||||
<a
|
||||
v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
|
||||
v-tooltip="primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'"
|
||||
:href="primaryFile.url"
|
||||
@click="emit('onDownload')"
|
||||
>
|
||||
@@ -320,7 +320,7 @@
|
||||
<FileIcon aria-hidden="true" />
|
||||
<span class="filename">
|
||||
<strong>{{ replaceFile.name }}</strong>
|
||||
<span class="file-size">({{ formatBytes(replaceFile.size) }})</span>
|
||||
<span class="file-size">({{ $formatBytes(replaceFile.size) }})</span>
|
||||
</span>
|
||||
<FileInput
|
||||
class="iconified-button raised-button"
|
||||
@@ -345,7 +345,7 @@
|
||||
<FileIcon aria-hidden="true" />
|
||||
<span class="filename">
|
||||
<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">
|
||||
Primary
|
||||
</span>
|
||||
@@ -412,7 +412,7 @@
|
||||
<FileIcon aria-hidden="true" />
|
||||
<span class="filename">
|
||||
<strong>{{ file.name }}</strong>
|
||||
<span class="file-size">({{ formatBytes(file.size) }})</span>
|
||||
<span class="file-size">({{ $formatBytes(file.size) }})</span>
|
||||
</span>
|
||||
<multiselect
|
||||
v-if="version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))"
|
||||
@@ -533,7 +533,7 @@
|
||||
)
|
||||
.map((it) => it.name)
|
||||
"
|
||||
:custom-label="formatCategory"
|
||||
:custom-label="(value) => $formatCategory(value)"
|
||||
:loading="tags.loaders.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
@@ -630,15 +630,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
CopyCode,
|
||||
Checkbox,
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
MarkdownEditor,
|
||||
} from "@modrinth/ui";
|
||||
import { ButtonStyled, ConfirmModal, MarkdownEditor } from "@modrinth/ui";
|
||||
import {
|
||||
FileIcon,
|
||||
TrashIcon,
|
||||
@@ -657,7 +649,6 @@ import {
|
||||
ChevronRightIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { formatBytes, formatCategory } from "@modrinth/utils";
|
||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||
import { inferVersionInfo } from "~/helpers/infer.js";
|
||||
import { createDataPackVersion } from "~/helpers/package.js";
|
||||
@@ -665,9 +656,13 @@ import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||
import { reportVersion } from "~/utils/report-helpers.ts";
|
||||
import { useImageUpload } from "~/composables/image-upload.ts";
|
||||
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import FileInput from "~/components/ui/FileInput.vue";
|
||||
import Modal from "~/components/ui/Modal.vue";
|
||||
|
||||
@@ -963,8 +958,6 @@ export default defineNuxtComponent({
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatBytes,
|
||||
formatCategory,
|
||||
async onImageUpload(file) {
|
||||
const response = await useImageUpload(file, { context: "version" });
|
||||
|
||||
|
||||
@@ -153,7 +153,10 @@
|
||||
{{ dayjs(charge.due).format("MMMM D, YYYY [at] h:mma") }}
|
||||
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
|
||||
</span>
|
||||
<div class="flex w-full items-center gap-1 text-xs text-secondary">
|
||||
<div
|
||||
v-if="flags.developerMode"
|
||||
class="flex w-full items-center gap-1 text-xs text-secondary"
|
||||
>
|
||||
{{ charge.status }}
|
||||
⋅
|
||||
{{ charge.type }}
|
||||
@@ -216,6 +219,7 @@ import dayjs from "dayjs";
|
||||
import { products } from "~/generated/state.json";
|
||||
import ModrinthServersIcon from "~/components/ui/servers/ModrinthServersIcon.vue";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const route = useRoute();
|
||||
const data = useNuxtApp();
|
||||
const vintl = useVIntl();
|
||||
@@ -285,13 +289,13 @@ const selectedCharge = ref(null);
|
||||
const refundType = ref("full");
|
||||
const refundTypes = ref(["full", "partial", "none"]);
|
||||
const refundAmount = ref(0);
|
||||
const unprovision = ref(true);
|
||||
const unprovision = ref(false);
|
||||
|
||||
function showRefundModal(charge) {
|
||||
selectedCharge.value = charge;
|
||||
refundType.value = "full";
|
||||
refundAmount.value = 0;
|
||||
unprovision.value = true;
|
||||
unprovision.value = false;
|
||||
refundModal.value.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import {
|
||||
DownloadIcon,
|
||||
LinkIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Avatar, Checkbox, Badge } from "@modrinth/ui";
|
||||
import Avatar from "~/components/ui/Avatar.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 ATLauncher from "~/assets/images/external/atlauncher.svg?component";
|
||||
import CurseForge from "~/assets/images/external/curseforge.svg?component";
|
||||
import LatestNewsRow from "~/components/ui/news/LatestNewsRow.vue";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
|
||||
import { homePageProjects } from "~/generated/state.json";
|
||||
|
||||
@@ -874,8 +875,101 @@ useSeoMeta({
|
||||
<nuxt-link to="/legal/privacy">Privacy Policy.</nuxt-link>
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-[var(--landing-raw-bg)]">
|
||||
<LatestNewsRow />
|
||||
<div class="logo-banner">
|
||||
<svg
|
||||
v-if="$theme.active === 'light'"
|
||||
viewBox="0 0 865 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="light-height"
|
||||
>
|
||||
<g clip-path="url(#clip0_419_237)">
|
||||
<rect x="176" width="512" height="512" fill="url(#paint0_linear_419_237)" />
|
||||
<g opacity="0.5">
|
||||
<path
|
||||
d="M176 274.069H219.65C221.89 299.001 228.25 322.42 238.87 345.249C245.4 342.033 251.37 338.016 257.52 334.3C263.76 330.534 270.07 326.908 276.62 323.061C276.21 321.669 275.95 320.216 275.37 318.914C271.05 309.378 268.47 299.251 266.32 289.114C265.03 283.054 264.41 276.743 263.73 270.513C262.89 262.86 262.94 255.257 263.11 247.654C263.26 241.083 264.11 234.482 265.21 227.991C266.88 218.064 269.39 208.308 272.94 198.852C276.79 188.594 281.19 178.588 287.21 169.382C291.19 163.292 294.97 157.021 299.53 151.382C306.3 142.998 313.75 135.214 322.12 128.273C330.05 121.692 338.16 115.421 347.18 110.483C354.15 106.676 361.39 103.341 368.66 100.115C379.13 95.4674 390.24 92.873 401.39 90.4289C410.67 88.4055 430.03 87.3237 438.96 88.6158C438.51 95.9282 436.63 103.05 435.44 110.262C434.26 117.394 432.94 124.496 431.6 132.109C428.15 132.44 424.73 132.7 421.32 133.121C416.25 133.752 411.15 134.263 406.15 135.265C395.4 137.428 385.13 141.044 375.33 145.973C366.85 150.23 358.98 155.398 351.89 161.709C348.8 164.464 345.52 167.048 342.67 170.033C335.77 177.225 329.99 185.279 324.68 193.713C320.48 200.364 317.45 207.536 314.8 214.839C312.05 222.422 310.33 230.315 308.95 238.308C307.43 247.093 307.9 255.898 308.2 264.653C308.48 272.867 310.18 281 312.49 288.934C313.45 292.239 314.44 295.535 315.62 299.481C332.06 289.705 348.08 280.169 364.47 270.422C361.92 263.631 359.46 257.16 357.05 250.669C354.63 244.178 352.26 237.667 349.78 230.926C352.79 227.871 355.71 224.936 358.6 221.971C369.97 210.291 381.46 198.712 392.6 186.821C395.77 183.436 399.36 181.913 403.66 181.062C414.16 178.978 424.63 176.734 435.11 174.541C441.87 173.128 448.62 171.686 455.69 170.193C461.36 177.175 467.08 184.217 472.94 191.439C471.8 192.671 470.85 193.783 469.82 194.815C463 201.656 455.97 208.308 449.41 215.4C446.18 218.896 442.34 220.709 438.01 221.961C433.09 223.383 428.16 224.745 423.25 226.198C422.5 226.418 421.73 226.909 421.19 227.48C416.62 232.298 412.15 237.216 407.55 241.995C405.9 243.697 405.89 245.31 406.67 247.424C408.91 253.474 410.92 259.604 413.12 265.674C413.5 266.716 414.27 267.668 415.04 268.499C419.55 273.377 424.05 278.266 428.73 282.974C429.48 283.725 431.24 284.055 432.33 283.775C438.57 282.182 444.72 280.289 450.94 278.636C453.49 277.955 455.32 276.443 457.01 274.559C460.1 271.094 463.55 267.898 466.27 264.172C469.55 259.684 473.91 257.901 479.09 256.82C483.44 255.908 487.58 254.045 491.83 252.673C496.23 251.25 500.69 249.998 505.07 248.495C507.08 247.804 508.18 248.305 508.99 250.198C512.16 257.671 515.4 265.114 518.76 272.917C515.97 276.342 513.04 279.918 510.13 283.515C505.45 289.304 500.81 295.124 496.12 300.904C492.16 305.782 487.86 310.42 484.26 315.549C481.66 319.255 478.09 320.717 474.16 321.959C462.33 325.716 450.52 329.502 438.69 333.268C431.61 335.522 424.51 337.756 416.91 340.16C415.33 338.648 413.45 337.055 411.81 335.232C407.16 330.053 402.62 324.794 398.03 319.565C395.63 316.831 393.3 314.006 390.79 311.382C388.08 308.557 387.52 308.557 384.41 310.42C377.88 314.337 371.33 318.243 364.82 322.19C356.32 327.328 347.85 332.507 339.09 337.836C341.26 341.482 344.39 344.257 347.49 346.811C353.22 351.539 359.26 355.907 365.37 360.144C376.53 367.867 388.92 372.815 402.07 376.081C409.82 378.004 417.59 379.286 425.6 379.076C426.74 379.046 427.88 379.446 429.02 379.516C430.64 379.617 432.27 379.667 433.88 379.587C435.19 379.526 436.48 379.106 437.79 379.076C442.03 378.966 442.07 378.996 443.29 383.443C446.74 396.014 450.16 408.585 453.58 421.167C453.7 421.597 453.65 422.068 453.7 422.88C449 423.28 444.42 423.781 439.83 424.022C436.17 424.212 432.5 423.961 428.83 424.092C414.81 424.593 401.08 422.469 387.69 418.733C366.28 412.763 346.56 403.216 328.88 389.523C320.7 383.183 313.41 375.95 306.16 368.648C304.89 367.366 303.97 365.743 302.78 364.371C300.52 361.746 299.87 361.526 296.85 363.189C292.49 365.583 288.24 368.167 283.98 370.722C277.72 374.458 271.49 378.234 265.25 382.001C264.56 382.421 263.91 382.902 263.18 383.403C263.7 387.54 267.09 389.654 269.19 392.448C272.55 396.946 276.45 401.113 280.58 404.939C286.77 410.669 293.38 415.938 299.77 421.457C306.94 427.658 314.86 432.756 322.98 437.604C332.25 443.144 341.84 448.092 351.94 451.808C361.7 455.394 371.65 458.54 381.69 461.244C388.49 463.077 395.62 463.678 402.56 465.061C414.21 467.385 425.99 467.224 437.75 466.924C444.49 466.754 451.21 465.622 457.94 465.071C465.69 464.44 473.25 462.767 480.79 461.014C492.41 458.319 503.64 454.453 514.64 449.795C530.77 442.963 545.63 433.968 559.65 423.601C570.63 415.477 580.59 406.061 589.5 395.774C598.25 385.667 606.72 375.209 612.97 363.219C615.56 358.241 618.38 353.382 621.25 348.184C635.52 353.322 649.65 358.411 664.03 363.59C663.75 364.691 663.65 365.663 663.26 366.484C655.92 381.77 647.35 396.285 636.95 409.727C628.93 420.105 620.71 430.292 611.17 439.307C604.8 445.327 598.41 451.358 591.25 456.496C584.53 461.314 578.16 466.653 571.24 471.151C556.34 480.857 540.49 488.721 523.9 495.262C508.29 501.412 492.07 505.198 475Line truncated
|
||||
fill="#05CE45"
|
||||
/>
|
||||
<path
|
||||
d="M688 277.074C687.69 277.615 687.11 278.136 687.09 278.686C686.56 293.612 683.03 308.016 679.73 322.45C675.99 322.05 642.32 309.939 636.41 306.894C637.6 300.173 638.85 293.431 639.97 286.66C641.13 279.648 641.76 272.606 642.1 265.484C642.43 258.563 642.1 251.691 642.02 244.789C641.94 237.858 641.47 231.006 639.48 223.784C625.94 227.39 612.76 230.896 599.27 234.492C599.19 235.964 598.74 237.507 599.09 238.839C600.63 244.749 599.88 250.739 599.99 256.699C600.43 280.079 595.08 302.226 585.83 323.592C580.73 335.372 574.24 346.311 566.66 356.598C559.95 365.713 552.43 374.228 543.81 381.53C534.36 389.543 524.76 397.497 513.41 402.896C508.18 405.39 503.03 408.045 497.83 410.599C497.43 410.799 496.9 410.759 496.49 410.819C494.33 407.514 486.2 378.274 484.59 368.177C533.62 341.532 557.58 300.664 554.95 244.639C548.26 194.314 521.33 159.555 474.14 139.752C476.69 125.278 479.28 110.593 481.96 95.3973C486 96.7796 489.75 97.8414 493.32 99.3439C504.19 103.932 514.89 108.93 524.68 115.541C537.67 124.316 549.44 134.653 559.65 146.473C570.52 159.055 579.39 173.078 585.96 188.434C587.1 191.099 587.3 191.279 589.95 190.578C602.53 187.272 615.11 183.917 628.08 180.481C627.53 178.608 627.18 176.845 626.5 175.232C618.95 157.262 609.28 140.493 597.33 125.107C590.3 116.052 582.71 107.398 574.08 99.8448C561.23 88.6058 547.51 78.599 532.54 70.1948C521.76 64.1446 510.5 59.3665 498.93 55.3798C488.2 51.6836 477.17 48.8788 465.88 46.9756C450.75 44.4313 435.58 43.7201 420.33 44.221C414.75 44.4013 409.21 45.6033 403.63 46.004C394.2 46.6851 385.07 48.979 375.97 51.2128C367.14 53.3764 358.57 56.4516 350.19 60.0276C346.54 61.5903 342.73 62.8324 339.19 64.6053C333.26 67.5804 327.36 70.6455 321.67 74.0713C316.29 77.3068 311.08 80.8628 306.03 84.5991C300.85 88.4255 295.84 92.5124 290.96 96.7195C286.31 100.726 281.67 104.793 277.47 109.261C267.13 120.259 257.6 131.919 250.05 145.061C246.17 151.812 242.21 158.564 238.88 165.596C231.64 180.881 226.13 196.788 222.94 213.466C222.13 217.674 220.91 221.79 220.92 226.148C220.92 227.36 220.13 228.582 219.6 230.084H176C176 229.083 176 228.081 176 227.089C176.32 226.548 176.92 226.008 176.9 225.477C176.77 220.298 178.02 215.33 178.92 210.291C181.03 198.581 183.94 187.072 187.69 175.803C189.88 169.212 192.51 162.771 195.06 156.31C199.14 145.933 204.4 136.146 210.05 126.57C215.44 117.444 221.43 108.74 227.82 100.265C233.72 92.4423 240.04 84.9797 246.83 77.9879C253.05 71.5771 260.16 66.0378 266.65 59.8774C274.64 52.3046 283.78 46.2644 292.92 40.3544C303.06 33.8034 313.58 27.8033 324.82 23.0953C335.82 18.4876 346.77 13.8598 358.24 10.4941C373.31 6.07663 388.68 3.1517 404.23 1.2585C411.77 0.336948 419.45 0.136611 427.06 0.206729C436.84 0.296881 446.65 -0.714826 456.41 1.05816C463.53 2.35034 470.77 2.98141 477.92 4.14337C489.5 6.02654 500.81 9.12177 511.97 12.6778C521.29 15.6428 530.2 19.6796 539.17 23.5661C548.81 27.7432 557.74 33.1222 566.71 38.4312C572.54 41.877 577.86 46.1642 583.55 49.8705C592.62 55.7704 600.49 63.1529 608.47 70.325C615 76.1949 620.72 82.9663 626.72 89.4172C628.86 91.7211 630.91 94.1352 632.73 96.6794C638.01 104.082 643.32 111.474 648.31 119.057C655.9 130.577 662.42 142.737 667.58 155.519C673.71 170.684 679.37 186.06 682.52 202.197C683.75 208.498 685.02 214.799 685.94 221.149C686.6 225.717 686.7 230.365 687.12 234.973C687.16 235.373 687.69 235.724 687.99 236.095V277.084L688 277.074Z"
|
||||
fill="#05CE45"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<rect y="194" width="865" height="318" fill="url(#paint1_linear_419_237)" />
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_419_237"
|
||||
x1="432"
|
||||
y1="0"
|
||||
x2="432"
|
||||
y2="512"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#E7FFEF" />
|
||||
<stop offset="0.678759" stop-color="white" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_419_237"
|
||||
x1="432.5"
|
||||
y1="194"
|
||||
x2="432.5"
|
||||
y2="315.861"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F9FFFB" stop-opacity="0" />
|
||||
<stop offset="1" stop-color="#F9FFFB" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_419_237">
|
||||
<rect x="176" width="512" height="512" rx="256" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_127_331)">
|
||||
<rect width="512" height="512" fill="url(#paint0_linear_127_331)" />
|
||||
<g style="mix-blend-mode: overlay">
|
||||
<g opacity="0.5">
|
||||
<path
|
||||
d="M0 274.069H43.65C45.89 299.001 52.25 322.42 62.87 345.249C69.4 342.033 75.37 338.016 81.52 334.3C87.76 330.534 94.07 326.908 100.62 323.061C100.21 321.669 99.95 320.216 99.37 318.914C95.05 309.378 92.47 299.251 90.32 289.114C89.03 283.054 88.41 276.743 87.73 270.513C86.89 262.86 86.94 255.257 87.11 247.654C87.26 241.083 88.11 234.482 89.21 227.991C90.88 218.064 93.39 208.308 96.94 198.852C100.79 188.594 105.19 178.588 111.21 169.382C115.19 163.292 118.97 157.021 123.53 151.382C130.3 142.998 137.75 135.214 146.12 128.273C154.05 121.692 162.16 115.421 171.18 110.483C178.15 106.676 185.39 103.341 192.66 100.115C203.13 95.4674 214.24 92.873 225.39 90.4289C234.67 88.4055 254.03 87.3237 262.96 88.6158C262.51 95.9282 260.63 103.05 259.44 110.262C258.26 117.394 256.94 124.496 255.6 132.109C252.15 132.44 248.73 132.7 245.32 133.121C240.25 133.752 235.15 134.263 230.15 135.265C219.4 137.428 209.13 141.044 199.33 145.973C190.85 150.23 182.98 155.398 175.89 161.709C172.8 164.464 169.52 167.048 166.67 170.033C159.77 177.225 153.99 185.279 148.68 193.713C144.48 200.364 141.45 207.536 138.8 214.839C136.05 222.422 134.33 230.315 132.95 238.308C131.43 247.093 131.9 255.898 132.2 264.653C132.48 272.867 134.18 281 136.49 288.934C137.45 292.239 138.44 295.535 139.62 299.481C156.06 289.705 172.08 280.169 188.47 270.422C185.92 263.631 183.46 257.16 181.05 250.669C178.63 244.178 176.26 237.667 173.78 230.926C176.79 227.871 179.71 224.936 182.6 221.971C193.97 210.291 205.46 198.712 216.6 186.821C219.77 183.436 223.36 181.913 227.66 181.062C238.16 178.978 248.63 176.734 259.11 174.541C265.87 173.128 272.62 171.686 279.69 170.193C285.36 177.175 291.08 184.217 296.94 191.439C295.8 192.671 294.85 193.783 293.82 194.815C287 201.656 279.97 208.308 273.41 215.4C270.18 218.896 266.34 220.709 262.01 221.961C257.09 223.383 252.16 224.745 247.25 226.198C246.5 226.418 245.73 226.909 245.19 227.48C240.62 232.298 236.15 237.216 231.55 241.995C229.9 243.697 229.89 245.31 230.67 247.424C232.91 253.474 234.92 259.604 237.12 265.674C237.5 266.716 238.27 267.668 239.04 268.499C243.55 273.377 248.05 278.266 252.73 282.974C253.48 283.725 255.24 284.055 256.33 283.775C262.57 282.182 268.72 280.289 274.94 278.636C277.49 277.955 279.32 276.443 281.01 274.559C284.1 271.094 287.55 267.898 290.27 264.172C293.55 259.684 297.91 257.901 303.09 256.82C307.44 255.908 311.58 254.045 315.83 252.673C320.23 251.25 324.69 249.998 329.07 248.495C331.08 247.804 332.18 248.305 332.99 250.198C336.16 257.671 339.4 265.114 342.76 272.917C339.97 276.342 337.04 279.919 334.13 283.515C329.45 289.304 324.81 295.124 320.12 300.904C316.16 305.782 311.86 310.42 308.26 315.549C305.66 319.255 302.09 320.717 298.16 321.959C286.33 325.716 274.52 329.502 262.69 333.268C255.61 335.522 248.51 337.756 240.91 340.16C239.33 338.648 237.45 337.055 235.81 335.232C231.16 330.053 226.62 324.794 222.03 319.565C219.63 316.831 217.3 314.006 214.79 311.382C212.08 308.557 211.52 308.557 208.41 310.42C201.88 314.337 195.33 318.243 188.82 322.19C180.32 327.328 171.85 332.507 163.09 337.836C165.26 341.482 168.39 344.257 171.49 346.811C177.22 351.539 183.26 355.907 189.37 360.144C200.53 367.867 212.92 372.815 226.07 376.081C233.82 378.004 241.59 379.286 249.6 379.076C250.74 379.046 251.88 379.446 253.02 379.516C254.64 379.617 256.27 379.667 257.88 379.587C259.19 379.526 260.48 379.106 261.79 379.076C266.03 378.966 266.07 378.996 267.29 383.443C270.74 396.014 274.16 408.585 277.58 421.167C277.7 421.597 277.65 422.068 277.7 422.88C273 423.28 268.42 423.781 263.83 424.022C260.17 424.212 256.5 423.961 252.83 424.092C238.81 424.592 225.08 422.469 211.69 418.733C190.28 412.763 170.56 403.216 152.88 389.523C144.7 383.183 137.41 375.95 130.16 368.648C128.89 367.366 127.97 365.743 126.78 364.371C124.52 361.746 123.87 361.526 120.85 363.189C116.49 365.583 112.24 368.167 107.98 370.722C101.72 374.458 95.49 378.234 89.25 382.001C88.56 382.421 87.91 382.902 87.18 383.403C87.7 387.54 91.09 389.654 93.19 392.448C96.55 396.946 100.45 401.113 104.58 404.939C110.77 410.669 117.38 415.938 123.77 421.457C130.94 427.658 138.86 432.756 146.98 437.604C156.25 443.144 165.84 448.092 175.94 451.808C185.7 455.394 195.65 458.54 205.69 461.244C212.49 463.077 219.62 463.678 226.56 465.061C238.21 467.385 249.99 467.224 261.75 466.924C268.49 466.754 275.21 465.622 281.94 465.071C289.69 464.44 297.25 462.767 304.79 461.014C316.41 458.319 327.64 454.453 338.64 449.795C354.77 442.964 369.63 433.968 383.65 423.601C394.63 415.477 404.59 406.061 413.5 395.774C422.25 385.667 430.72 375.209 436.97 363.219C439.56 358.241 442.38 353.382 445.25 348.184C459.52 353.322 473.65 358.411 488.03 363.59C487.75 364.691 487.65 365.663 487.26 366.484C479.92 381.77 471.35 396.285 460.95 409.727C452.93 420.105 444.71 430.292 435.17 439.307C428.8 445.327 422.41 451.358 415.25 456.496C408.53 461.314 402.16 466.653 395.24 471.151C380.34 480.857 364.49 488.721 347.9 495.262C332.29 501.412 316.07 505.198 299.79 508.774C292.91 510.287 285.7 51Line truncated
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M512 277.074C511.69 277.615 511.11 278.136 511.09 278.686C510.56 293.612 507.03 308.016 503.73 322.45C499.99 322.05 466.32 309.939 460.41 306.894C461.6 300.173 462.85 293.431 463.97 286.66C465.13 279.648 465.76 272.606 466.1 265.484C466.43 258.563 466.1 251.691 466.02 244.789C465.94 237.858 465.47 231.006 463.48 223.784C449.94 227.39 436.76 230.896 423.27 234.492C423.19 235.964 422.74 237.507 423.09 238.839C424.63 244.749 423.88 250.739 423.99 256.699C424.43 280.079 419.08 302.226 409.83 323.592C404.73 335.372 398.24 346.311 390.66 356.598C383.95 365.713 376.43 374.228 367.81 381.53C358.36 389.543 348.76 397.497 337.41 402.896C332.18 405.39 327.03 408.045 321.83 410.599C321.43 410.799 320.9 410.759 320.49 410.819C318.33 407.514 310.2 378.274 308.59 368.177C357.62 341.532 381.58 300.664 378.95 244.639C372.26 194.314 345.33 159.555 298.14 139.752C300.69 125.278 303.28 110.593 305.96 95.3973C310 96.7796 313.75 97.8414 317.32 99.3439C328.19 103.932 338.89 108.93 348.68 115.541C361.67 124.316 373.44 134.654 383.65 146.473C394.52 159.055 403.39 173.078 409.96 188.434C411.1 191.099 411.3 191.279 413.95 190.578C426.53 187.272 439.11 183.917 452.08 180.481C451.53 178.608 451.18 176.845 450.5 175.232C442.95 157.262 433.28 140.493 421.33 125.107C414.3 116.052 406.71 107.398 398.08 99.8448C385.23 88.6058 371.51 78.599 356.54 70.1948C345.76 64.1446 334.5 59.3665 322.93 55.3798C312.2 51.6836 301.17 48.8788 289.88 46.9756C274.75 44.4313 259.58 43.7201 244.33 44.221C238.75 44.4013 233.21 45.6033 227.63 46.004C218.2 46.6851 209.07 48.979 199.97 51.2128C191.14 53.3764 182.57 56.4516 174.19 60.0276C170.54 61.5903 166.73 62.8324 163.19 64.6053C157.26 67.5804 151.36 70.6455 145.67 74.0713C140.29 77.3068 135.08 80.8628 130.03 84.5991C124.85 88.4255 119.84 92.5124 114.96 96.7195C110.31 100.726 105.67 104.793 101.47 109.261C91.13 120.259 81.6 131.919 74.05 145.061C70.17 151.812 66.21 158.564 62.88 165.596C55.64 180.881 50.13 196.788 46.94 213.466C46.13 217.674 44.91 221.79 44.92 226.148C44.92 227.36 44.13 228.582 43.6 230.084H0C0 229.083 0 228.081 0 227.089C0.32 226.548 0.92 226.008 0.9 225.477C0.77 220.298 2.02 215.33 2.92 210.291C5.03 198.581 7.94 187.072 11.69 175.803C13.88 169.212 16.51 162.771 19.06 156.31C23.14 145.933 28.4 136.146 34.05 126.57C39.44 117.444 45.43 108.74 51.82 100.265C57.72 92.4423 64.04 84.9797 70.83 77.9879C77.05 71.5771 84.16 66.0378 90.65 59.8774C98.64 52.3046 107.78 46.2644 116.92 40.3544C127.06 33.8034 137.58 27.8033 148.82 23.0953C159.82 18.4876 170.77 13.8598 182.24 10.4941C197.31 6.07663 212.68 3.1517 228.23 1.2585C235.77 0.336948 243.45 0.136611 251.06 0.206729C260.84 0.296881 270.65 -0.714826 280.41 1.05816C287.53 2.35034 294.77 2.98141 301.92 4.14337C313.5 6.02654 324.81 9.12177 335.97 12.6778C345.29 15.6428 354.2 19.6796 363.17 23.5661C372.81 27.7432 381.74 33.1222 390.71 38.4312C396.54 41.877 401.86 46.1642 407.55 49.8705C416.62 55.7705 424.49 63.1529 432.47 70.325C439 76.1949 444.72 82.9663 450.72 89.4172C452.86 91.7211 454.91 94.1352 456.73 96.6795C462.01 104.082 467.32 111.474 472.31 119.057C479.9 130.577 486.42 142.737 491.58 155.519C497.71 170.684 503.37 186.06 506.52 202.197C507.75 208.498 509.02 214.799 509.94 221.149C510.6 225.717 510.7 230.365 511.12 234.973C511.16 235.373 511.69 235.724 511.99 236.095V277.084L512 277.074Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_127_331"
|
||||
x1="256"
|
||||
y1="0"
|
||||
x2="256"
|
||||
y2="512"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#05CE45" />
|
||||
<stop offset="0.678759" stop-color="#051F10" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_127_331">
|
||||
<rect width="512" height="512" rx="256" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="overlay">
|
||||
<h2 class="main-header">
|
||||
Read more about <br />
|
||||
<strong class="main-header-strong">Modrinth</strong>
|
||||
</h2>
|
||||
<a
|
||||
href="https://blog.modrinth.com/?utm_source=website&utm_source=homepage&utm_campaign=newsletter"
|
||||
class="iconified-button brand-button"
|
||||
>
|
||||
Visit the blog
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -248,7 +248,9 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<AdPlaceholder v-if="!auth.user" />
|
||||
<AdPlaceholder
|
||||
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
|
||||
/>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<nav class="navigation-card">
|
||||
@@ -490,6 +492,7 @@ const route = useNativeRoute();
|
||||
const auth = await useAuth();
|
||||
const cosmetics = useCosmetics();
|
||||
const tags = useTags();
|
||||
const flags = useFeatureFlags();
|
||||
|
||||
const isEditing = ref(false);
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets";
|
||||
import { Avatar } from "@modrinth/ui";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
||||
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts";
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
v-if="notifTypes.length > 1"
|
||||
v-model="selectedType"
|
||||
:items="notifTypes"
|
||||
:format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')"
|
||||
:format-label="(x) => (x === 'all' ? 'All' : $formatProjectType(x).replace('_', ' ') + 's')"
|
||||
:capitalize="false"
|
||||
/>
|
||||
<p v-if="pending">Loading notifications...</p>
|
||||
@@ -49,16 +49,13 @@
|
||||
/>
|
||||
</template>
|
||||
<p v-else>You don't have any unread notifications.</p>
|
||||
<div class="flex justify-end">
|
||||
<Pagination :page="page" :count="pages" @switch-page="changePage" />
|
||||
</div>
|
||||
<Pagination :page="page" :count="pages" @switch-page="changePage" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, Pagination, Chips } from "@modrinth/ui";
|
||||
import { Button, Chips } from "@modrinth/ui";
|
||||
import { HistoryIcon, CheckCheckIcon } from "@modrinth/assets";
|
||||
import { formatProjectType } from "@modrinth/utils";
|
||||
import {
|
||||
fetchExtraNotificationData,
|
||||
groupNotifications,
|
||||
@@ -66,6 +63,7 @@ import {
|
||||
} from "~/helpers/notifications.ts";
|
||||
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import Pagination from "~/components/ui/Pagination.vue";
|
||||
|
||||
useHead({
|
||||
title: "Notifications - Modrinth",
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
/>
|
||||
<div class="push-right input-group">
|
||||
<button class="iconified-button" @click="$refs.editLinksModal.hide()">
|
||||
<XIcon />
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="bulkEditLinks()">
|
||||
@@ -199,8 +199,8 @@
|
||||
class="square-button"
|
||||
@click="updateDescending()"
|
||||
>
|
||||
<SortDescIcon v-if="descending" />
|
||||
<SortAscIcon v-else />
|
||||
<DescendingIcon v-if="descending" />
|
||||
<AscendingIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,7 +239,7 @@
|
||||
<div>
|
||||
<nuxt-link
|
||||
tabindex="-1"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
>
|
||||
@@ -261,7 +261,7 @@
|
||||
|
||||
<nuxt-link
|
||||
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
|
||||
}`"
|
||||
>
|
||||
@@ -275,23 +275,22 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ formatProjectType(getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
||||
{{ $formatProjectType($getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ProjectStatusBadge v-if="project.status" :status="project.status" />
|
||||
<Badge v-if="project.status" :type="project.status" class="status" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<nuxt-link
|
||||
class="square-button"
|
||||
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,46 +305,40 @@ import {
|
||||
SettingsIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
XIcon,
|
||||
XIcon as CrossIcon,
|
||||
IssuesIcon,
|
||||
EditIcon,
|
||||
SaveIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
SortAscendingIcon as AscendingIcon,
|
||||
SortDescendingIcon as DescendingIcon,
|
||||
} from "@modrinth/assets";
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
CopyCode,
|
||||
ProjectStatusBadge,
|
||||
commonMessages,
|
||||
} from "@modrinth/ui";
|
||||
import { formatProjectType } from "@modrinth/utils";
|
||||
import { commonMessages } from "@modrinth/ui";
|
||||
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import Modal from "~/components/ui/Modal.vue";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
||||
import { getProjectTypeForUrl } from "~/helpers/projects.js";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
ProjectStatusBadge,
|
||||
Badge,
|
||||
SettingsIcon,
|
||||
TrashIcon,
|
||||
Checkbox,
|
||||
IssuesIcon,
|
||||
PlusIcon,
|
||||
XIcon,
|
||||
CrossIcon,
|
||||
EditIcon,
|
||||
SaveIcon,
|
||||
Modal,
|
||||
ModalCreation,
|
||||
Multiselect,
|
||||
CopyCode,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
AscendingIcon,
|
||||
DescendingIcon,
|
||||
},
|
||||
async setup() {
|
||||
const { formatMessage } = useVIntl();
|
||||
@@ -397,8 +390,6 @@ export default defineNuxtComponent({
|
||||
this.DELETE_PROJECT = 1 << 7;
|
||||
},
|
||||
methods: {
|
||||
getProjectTypeForUrl,
|
||||
formatProjectType,
|
||||
updateDescending() {
|
||||
this.descending = !this.descending;
|
||||
this.projects = this.updateSort(this.projects, this.sortBy, this.descending);
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</span>
|
||||
<template v-if="payout.method">
|
||||
<span>⋅</span>
|
||||
<span>{{ formatWallet(payout.method) }} ({{ payout.method_address }})</span>
|
||||
<span>{{ $formatWallet(payout.method) }} ({{ payout.method_address }})</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { XIcon, PayPalIcon, UnknownIcon } from "@modrinth/assets";
|
||||
import { capitalizeString, formatWallet } from "@modrinth/utils";
|
||||
import { capitalizeString } from "@modrinth/utils";
|
||||
import { Badge, Breadcrumbs, DropdownSelect } from "@modrinth/ui";
|
||||
import dayjs from "dayjs";
|
||||
import TremendousIcon from "~/assets/images/external/tremendous.svg?component";
|
||||
|
||||
@@ -139,8 +139,8 @@
|
||||
<template v-if="knownErrors.length === 0 && amount">
|
||||
<Checkbox v-if="fees > 0" v-model="agreedFees" description="Consent to fee">
|
||||
I acknowledge that an estimated
|
||||
{{ formatMoney(fees) }} will be deducted from the amount I receive to cover
|
||||
{{ formatWallet(selectedMethod.type) }} processing fees.
|
||||
{{ $formatMoney(fees) }} will be deducted from the amount I receive to cover
|
||||
{{ $formatWallet(selectedMethod.type) }} processing fees.
|
||||
</Checkbox>
|
||||
<Checkbox v-model="agreedTransfer" description="Confirm transfer">
|
||||
<template v-if="selectedMethod.type === 'tremendous'">
|
||||
@@ -149,7 +149,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
I confirm that I am initiating a transfer to the following
|
||||
{{ formatWallet(selectedMethod.type) }} account: {{ withdrawAccount }}
|
||||
{{ $formatWallet(selectedMethod.type) }} account: {{ withdrawAccount }}
|
||||
</template>
|
||||
</Checkbox>
|
||||
<Checkbox v-model="agreedTerms" class="rewards-checkbox">
|
||||
@@ -198,7 +198,6 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
import { Chips, Checkbox, Breadcrumbs } from "@modrinth/ui";
|
||||
import { all } from "iso-3166-1";
|
||||
import { formatMoney, formatWallet } from "@modrinth/utils";
|
||||
import VenmoIcon from "~/assets/images/external/venmo.svg?component";
|
||||
|
||||
const auth = await useAuth();
|
||||
@@ -361,7 +360,9 @@ async function withdraw() {
|
||||
text:
|
||||
selectedMethod.value.type === "tremendous"
|
||||
? "An email has been sent to your account with further instructions on how to redeem your payout!"
|
||||
: `Payment has been sent to your ${formatWallet(selectedMethod.value.type)} account!`,
|
||||
: `Payment has been sent to your ${data.$formatWallet(
|
||||
selectedMethod.value.type,
|
||||
)} account!`,
|
||||
type: "success",
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -419,27 +419,128 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-[var(--landing-raw-bg)]">
|
||||
<LatestNewsRow />
|
||||
<div class="logo-banner">
|
||||
<svg
|
||||
v-if="$theme.active === 'light'"
|
||||
viewBox="0 0 865 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="light-height modrinth-icon"
|
||||
>
|
||||
<g clip-path="url(#clip0_419_237)">
|
||||
<rect x="176" width="512" height="512" fill="url(#paint0_linear_419_237)" />
|
||||
<g opacity="0.5">
|
||||
<path
|
||||
d="M176 274.069H219.65C221.89 299.001 228.25 322.42 238.87 345.249C245.4 342.033 251.37 338.016 257.52 334.3C263.76 330.534 270.07 326.908 276.62 323.061C276.21 321.669 275.95 320.216 275.37 318.914C271.05 309.378 268.47 299.251 266.32 289.114C265.03 283.054 264.41 276.743 263.73 270.513C262.89 262.86 262.94 255.257 263.11 247.654C263.26 241.083 264.11 234.482 265.21 227.991C266.88 218.064 269.39 208.308 272.94 198.852C276.79 188.594 281.19 178.588 287.21 169.382C291.19 163.292 294.97 157.021 299.53 151.382C306.3 142.998 313.75 135.214 322.12 128.273C330.05 121.692 338.16 115.421 347.18 110.483C354.15 106.676 361.39 103.341 368.66 100.115C379.13 95.4674 390.24 92.873 401.39 90.4289C410.67 88.4055 430.03 87.3237 438.96 88.6158C438.51 95.9282 436.63 103.05 435.44 110.262C434.26 117.394 432.94 124.496 431.6 132.109C428.15 132.44 424.73 132.7 421.32 133.121C416.25 133.752 411.15 134.263 406.15 135.265C395.4 137.428 385.13 141.044 375.33 145.973C366.85 150.23 358.98 155.398 351.89 161.709C348.8 164.464 345.52 167.048 342.67 170.033C335.77 177.225 329.99 185.279 324.68 193.713C320.48 200.364 317.45 207.536 314.8 214.839C312.05 222.422 310.33 230.315 308.95 238.308C307.43 247.093 307.9 255.898 308.2 264.653C308.48 272.867 310.18 281 312.49 288.934C313.45 292.239 314.44 295.535 315.62 299.481C332.06 289.705 348.08 280.169 364.47 270.422C361.92 263.631 359.46 257.16 357.05 250.669C354.63 244.178 352.26 237.667 349.78 230.926C352.79 227.871 355.71 224.936 358.6 221.971C369.97 210.291 381.46 198.712 392.6 186.821C395.77 183.436 399.36 181.913 403.66 181.062C414.16 178.978 424.63 176.734 435.11 174.541C441.87 173.128 448.62 171.686 455.69 170.193C461.36 177.175 467.08 184.217 472.94 191.439C471.8 192.671 470.85 193.783 469.82 194.815C463 201.656 455.97 208.308 449.41 215.4C446.18 218.896 442.34 220.709 438.01 221.961C433.09 223.383 428.16 224.745 423.25 226.198C422.5 226.418 421.73 226.909 421.19 227.48C416.62 232.298 412.15 237.216 407.55 241.995C405.9 243.697 405.89 245.31 406.67 247.424C408.91 253.474 410.92 259.604 413.12 265.674C413.5 266.716 414.27 267.668 415.04 268.499C419.55 273.377 424.05 278.266 428.73 282.974C429.48 283.725 431.24 284.055 432.33 283.775C438.57 282.182 444.72 280.289 450.94 278.636C453.49 277.955 455.32 276.443 457.01 274.559C460.1 271.094 463.55 267.898 466.27 264.172C469.55 259.684 473.91 257.901 479.09 256.82C483.44 255.908 487.58 254.045 491.83 252.673C496.23 251.25 500.69 249.998 505.07 248.495C507.08 247.804 508.18 248.305 508.99 250.198C512.16 257.671 515.4 265.114 518.76 272.917C515.97 276.342 513.04 279.918 510.13 283.515C505.45 289.304 500.81 295.124 496.12 300.904C492.16 305.782 487.86 310.42 484.26 315.549C481.66 319.255 478.09 320.717 474.16 321.959C462.33 325.716 450.52 329.502 438.69 333.268C431.61 335.522 424.51 337.756 416.91 340.16C415.33 338.648 413.45 337.055 411.81 335.232C407.16 330.053 402.62 324.794 398.03 319.565C395.63 316.831 393.3 314.006 390.79 311.382C388.08 308.557 387.52 308.557 384.41 310.42C377.88 314.337 371.33 318.243 364.82 322.19C356.32 327.328 347.85 332.507 339.09 337.836C341.26 341.482 344.39 344.257 347.49 346.811C353.22 351.539 359.26 355.907 365.37 360.144C376.53 367.867 388.92 372.815 402.07 376.081C409.82 378.004 417.59 379.286 425.6 379.076C426.74 379.046 427.88 379.446 429.02 379.516C430.64 379.617 432.27 379.667 433.88 379.587C435.19 379.526 436.48 379.106 437.79 379.076C442.03 378.966 442.07 378.996 443.29 383.443C446.74 396.014 450.16 408.585 453.58 421.167C453.7 421.597 453.65 422.068 453.7 422.88C449 423.28 444.42 423.781 439.83 424.022C436.17 424.212 432.5 423.961 428.83 424.092C414.81 424.593 401.08 422.469 387.69 418.733C366.28 412.763 346.56 403.216 328.88 389.523C320.7 383.183 313.41 375.95 306.16 368.648C304.89 367.366 303.97 365.743 302.78 364.371C300.52 361.746 299.87 361.526 296.85 363.189C292.49 365.583 288.24 368.167 283.98 370.722C277.72 374.458 271.49 378.234 265.25 382.001C264.56 382.421 263.91 382.902 263.18 383.403C263.7 387.54 267.09 389.654 269.19 392.448C272.55 396.946 276.45 401.113 280.58 404.939C286.77 410.669 293.38 415.938 299.77 421.457C306.94 427.658 314.86 432.756 322.98 437.604C332.25 443.144 341.84 448.092 351.94 451.808C361.7 455.394 371.65 458.54 381.69 461.244C388.49 463.077 395.62 463.678 402.56 465.061C414.21 467.385 425.99 467.224 437.75 466.924C444.49 466.754 451.21 465.622 457.94 465.071C465.69 464.44 473.25 462.767 480.79 461.014C492.41 458.319 503.64 454.453 514.64 449.795C530.77 442.963 545.63 433.968 559.65 423.601C570.63 415.477 580.59 406.061 589.5 395.774C598.25 385.667 606.72 375.209 612.97 363.219C615.56 358.241 618.38 353.382 621.25 348.184C635.52 353.322 649.65 358.411 664.03 363.59C663.75 364.691 663.65 365.663 663.26 366.484C655.92 381.77 647.35 396.285 636.95 409.727C628.93 420.105 620.71 430.292 611.17 439.307C604.8 445.327 598.41 451.358 591.25 456.496C584.53 461.314 578.16 466.653 571.24 471.151C556.34 480.857 540.49 488.721 523.9 495.262C508.29 501.412 492.07 505.198 475Line truncated
|
||||
fill="#05CE45"
|
||||
/>
|
||||
<path
|
||||
d="M688 277.074C687.69 277.615 687.11 278.136 687.09 278.686C686.56 293.612 683.03 308.016 679.73 322.45C675.99 322.05 642.32 309.939 636.41 306.894C637.6 300.173 638.85 293.431 639.97 286.66C641.13 279.648 641.76 272.606 642.1 265.484C642.43 258.563 642.1 251.691 642.02 244.789C641.94 237.858 641.47 231.006 639.48 223.784C625.94 227.39 612.76 230.896 599.27 234.492C599.19 235.964 598.74 237.507 599.09 238.839C600.63 244.749 599.88 250.739 599.99 256.699C600.43 280.079 595.08 302.226 585.83 323.592C580.73 335.372 574.24 346.311 566.66 356.598C559.95 365.713 552.43 374.228 543.81 381.53C534.36 389.543 524.76 397.497 513.41 402.896C508.18 405.39 503.03 408.045 497.83 410.599C497.43 410.799 496.9 410.759 496.49 410.819C494.33 407.514 486.2 378.274 484.59 368.177C533.62 341.532 557.58 300.664 554.95 244.639C548.26 194.314 521.33 159.555 474.14 139.752C476.69 125.278 479.28 110.593 481.96 95.3973C486 96.7796 489.75 97.8414 493.32 99.3439C504.19 103.932 514.89 108.93 524.68 115.541C537.67 124.316 549.44 134.653 559.65 146.473C570.52 159.055 579.39 173.078 585.96 188.434C587.1 191.099 587.3 191.279 589.95 190.578C602.53 187.272 615.11 183.917 628.08 180.481C627.53 178.608 627.18 176.845 626.5 175.232C618.95 157.262 609.28 140.493 597.33 125.107C590.3 116.052 582.71 107.398 574.08 99.8448C561.23 88.6058 547.51 78.599 532.54 70.1948C521.76 64.1446 510.5 59.3665 498.93 55.3798C488.2 51.6836 477.17 48.8788 465.88 46.9756C450.75 44.4313 435.58 43.7201 420.33 44.221C414.75 44.4013 409.21 45.6033 403.63 46.004C394.2 46.6851 385.07 48.979 375.97 51.2128C367.14 53.3764 358.57 56.4516 350.19 60.0276C346.54 61.5903 342.73 62.8324 339.19 64.6053C333.26 67.5804 327.36 70.6455 321.67 74.0713C316.29 77.3068 311.08 80.8628 306.03 84.5991C300.85 88.4255 295.84 92.5124 290.96 96.7195C286.31 100.726 281.67 104.793 277.47 109.261C267.13 120.259 257.6 131.919 250.05 145.061C246.17 151.812 242.21 158.564 238.88 165.596C231.64 180.881 226.13 196.788 222.94 213.466C222.13 217.674 220.91 221.79 220.92 226.148C220.92 227.36 220.13 228.582 219.6 230.084H176C176 229.083 176 228.081 176 227.089C176.32 226.548 176.92 226.008 176.9 225.477C176.77 220.298 178.02 215.33 178.92 210.291C181.03 198.581 183.94 187.072 187.69 175.803C189.88 169.212 192.51 162.771 195.06 156.31C199.14 145.933 204.4 136.146 210.05 126.57C215.44 117.444 221.43 108.74 227.82 100.265C233.72 92.4423 240.04 84.9797 246.83 77.9879C253.05 71.5771 260.16 66.0378 266.65 59.8774C274.64 52.3046 283.78 46.2644 292.92 40.3544C303.06 33.8034 313.58 27.8033 324.82 23.0953C335.82 18.4876 346.77 13.8598 358.24 10.4941C373.31 6.07663 388.68 3.1517 404.23 1.2585C411.77 0.336948 419.45 0.136611 427.06 0.206729C436.84 0.296881 446.65 -0.714826 456.41 1.05816C463.53 2.35034 470.77 2.98141 477.92 4.14337C489.5 6.02654 500.81 9.12177 511.97 12.6778C521.29 15.6428 530.2 19.6796 539.17 23.5661C548.81 27.7432 557.74 33.1222 566.71 38.4312C572.54 41.877 577.86 46.1642 583.55 49.8705C592.62 55.7704 600.49 63.1529 608.47 70.325C615 76.1949 620.72 82.9663 626.72 89.4172C628.86 91.7211 630.91 94.1352 632.73 96.6794C638.01 104.082 643.32 111.474 648.31 119.057C655.9 130.577 662.42 142.737 667.58 155.519C673.71 170.684 679.37 186.06 682.52 202.197C683.75 208.498 685.02 214.799 685.94 221.149C686.6 225.717 686.7 230.365 687.12 234.973C687.16 235.373 687.69 235.724 687.99 236.095V277.084L688 277.074Z"
|
||||
fill="#05CE45"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<rect y="194" width="865" height="318" fill="url(#paint1_linear_419_237)" />
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_419_237"
|
||||
x1="432"
|
||||
y1="0"
|
||||
x2="432"
|
||||
y2="512"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#E7FFEF" />
|
||||
<stop offset="0.678759" stop-color="white" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_419_237"
|
||||
x1="432.5"
|
||||
y1="194"
|
||||
x2="432.5"
|
||||
y2="315.861"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F9FFFB" stop-opacity="0" />
|
||||
<stop offset="1" stop-color="#F9FFFB" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_419_237">
|
||||
<rect x="176" width="512" height="512" rx="256" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="modrinth-icon"
|
||||
>
|
||||
<g clip-path="url(#clip0_127_331)">
|
||||
<rect width="512" height="512" fill="url(#paint0_linear_127_331)" />
|
||||
<g style="mix-blend-mode: overlay">
|
||||
<g opacity="0.5">
|
||||
<path
|
||||
d="M0 274.069H43.65C45.89 299.001 52.25 322.42 62.87 345.249C69.4 342.033 75.37 338.016 81.52 334.3C87.76 330.534 94.07 326.908 100.62 323.061C100.21 321.669 99.95 320.216 99.37 318.914C95.05 309.378 92.47 299.251 90.32 289.114C89.03 283.054 88.41 276.743 87.73 270.513C86.89 262.86 86.94 255.257 87.11 247.654C87.26 241.083 88.11 234.482 89.21 227.991C90.88 218.064 93.39 208.308 96.94 198.852C100.79 188.594 105.19 178.588 111.21 169.382C115.19 163.292 118.97 157.021 123.53 151.382C130.3 142.998 137.75 135.214 146.12 128.273C154.05 121.692 162.16 115.421 171.18 110.483C178.15 106.676 185.39 103.341 192.66 100.115C203.13 95.4674 214.24 92.873 225.39 90.4289C234.67 88.4055 254.03 87.3237 262.96 88.6158C262.51 95.9282 260.63 103.05 259.44 110.262C258.26 117.394 256.94 124.496 255.6 132.109C252.15 132.44 248.73 132.7 245.32 133.121C240.25 133.752 235.15 134.263 230.15 135.265C219.4 137.428 209.13 141.044 199.33 145.973C190.85 150.23 182.98 155.398 175.89 161.709C172.8 164.464 169.52 167.048 166.67 170.033C159.77 177.225 153.99 185.279 148.68 193.713C144.48 200.364 141.45 207.536 138.8 214.839C136.05 222.422 134.33 230.315 132.95 238.308C131.43 247.093 131.9 255.898 132.2 264.653C132.48 272.867 134.18 281 136.49 288.934C137.45 292.239 138.44 295.535 139.62 299.481C156.06 289.705 172.08 280.169 188.47 270.422C185.92 263.631 183.46 257.16 181.05 250.669C178.63 244.178 176.26 237.667 173.78 230.926C176.79 227.871 179.71 224.936 182.6 221.971C193.97 210.291 205.46 198.712 216.6 186.821C219.77 183.436 223.36 181.913 227.66 181.062C238.16 178.978 248.63 176.734 259.11 174.541C265.87 173.128 272.62 171.686 279.69 170.193C285.36 177.175 291.08 184.217 296.94 191.439C295.8 192.671 294.85 193.783 293.82 194.815C287 201.656 279.97 208.308 273.41 215.4C270.18 218.896 266.34 220.709 262.01 221.961C257.09 223.383 252.16 224.745 247.25 226.198C246.5 226.418 245.73 226.909 245.19 227.48C240.62 232.298 236.15 237.216 231.55 241.995C229.9 243.697 229.89 245.31 230.67 247.424C232.91 253.474 234.92 259.604 237.12 265.674C237.5 266.716 238.27 267.668 239.04 268.499C243.55 273.377 248.05 278.266 252.73 282.974C253.48 283.725 255.24 284.055 256.33 283.775C262.57 282.182 268.72 280.289 274.94 278.636C277.49 277.955 279.32 276.443 281.01 274.559C284.1 271.094 287.55 267.898 290.27 264.172C293.55 259.684 297.91 257.901 303.09 256.82C307.44 255.908 311.58 254.045 315.83 252.673C320.23 251.25 324.69 249.998 329.07 248.495C331.08 247.804 332.18 248.305 332.99 250.198C336.16 257.671 339.4 265.114 342.76 272.917C339.97 276.342 337.04 279.919 334.13 283.515C329.45 289.304 324.81 295.124 320.12 300.904C316.16 305.782 311.86 310.42 308.26 315.549C305.66 319.255 302.09 320.717 298.16 321.959C286.33 325.716 274.52 329.502 262.69 333.268C255.61 335.522 248.51 337.756 240.91 340.16C239.33 338.648 237.45 337.055 235.81 335.232C231.16 330.053 226.62 324.794 222.03 319.565C219.63 316.831 217.3 314.006 214.79 311.382C212.08 308.557 211.52 308.557 208.41 310.42C201.88 314.337 195.33 318.243 188.82 322.19C180.32 327.328 171.85 332.507 163.09 337.836C165.26 341.482 168.39 344.257 171.49 346.811C177.22 351.539 183.26 355.907 189.37 360.144C200.53 367.867 212.92 372.815 226.07 376.081C233.82 378.004 241.59 379.286 249.6 379.076C250.74 379.046 251.88 379.446 253.02 379.516C254.64 379.617 256.27 379.667 257.88 379.587C259.19 379.526 260.48 379.106 261.79 379.076C266.03 378.966 266.07 378.996 267.29 383.443C270.74 396.014 274.16 408.585 277.58 421.167C277.7 421.597 277.65 422.068 277.7 422.88C273 423.28 268.42 423.781 263.83 424.022C260.17 424.212 256.5 423.961 252.83 424.092C238.81 424.592 225.08 422.469 211.69 418.733C190.28 412.763 170.56 403.216 152.88 389.523C144.7 383.183 137.41 375.95 130.16 368.648C128.89 367.366 127.97 365.743 126.78 364.371C124.52 361.746 123.87 361.526 120.85 363.189C116.49 365.583 112.24 368.167 107.98 370.722C101.72 374.458 95.49 378.234 89.25 382.001C88.56 382.421 87.91 382.902 87.18 383.403C87.7 387.54 91.09 389.654 93.19 392.448C96.55 396.946 100.45 401.113 104.58 404.939C110.77 410.669 117.38 415.938 123.77 421.457C130.94 427.658 138.86 432.756 146.98 437.604C156.25 443.144 165.84 448.092 175.94 451.808C185.7 455.394 195.65 458.54 205.69 461.244C212.49 463.077 219.62 463.678 226.56 465.061C238.21 467.385 249.99 467.224 261.75 466.924C268.49 466.754 275.21 465.622 281.94 465.071C289.69 464.44 297.25 462.767 304.79 461.014C316.41 458.319 327.64 454.453 338.64 449.795C354.77 442.964 369.63 433.968 383.65 423.601C394.63 415.477 404.59 406.061 413.5 395.774C422.25 385.667 430.72 375.209 436.97 363.219C439.56 358.241 442.38 353.382 445.25 348.184C459.52 353.322 473.65 358.411 488.03 363.59C487.75 364.691 487.65 365.663 487.26 366.484C479.92 381.77 471.35 396.285 460.95 409.727C452.93 420.105 444.71 430.292 435.17 439.307C428.8 445.327 422.41 451.358 415.25 456.496C408.53 461.314 402.16 466.653 395.24 471.151C380.34 480.857 364.49 488.721 347.9 495.262C332.29 501.412 316.07 505.198 299.79 508.774C292.91 510.287 285.7 51Line truncated
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M512 277.074C511.69 277.615 511.11 278.136 511.09 278.686C510.56 293.612 507.03 308.016 503.73 322.45C499.99 322.05 466.32 309.939 460.41 306.894C461.6 300.173 462.85 293.431 463.97 286.66C465.13 279.648 465.76 272.606 466.1 265.484C466.43 258.563 466.1 251.691 466.02 244.789C465.94 237.858 465.47 231.006 463.48 223.784C449.94 227.39 436.76 230.896 423.27 234.492C423.19 235.964 422.74 237.507 423.09 238.839C424.63 244.749 423.88 250.739 423.99 256.699C424.43 280.079 419.08 302.226 409.83 323.592C404.73 335.372 398.24 346.311 390.66 356.598C383.95 365.713 376.43 374.228 367.81 381.53C358.36 389.543 348.76 397.497 337.41 402.896C332.18 405.39 327.03 408.045 321.83 410.599C321.43 410.799 320.9 410.759 320.49 410.819C318.33 407.514 310.2 378.274 308.59 368.177C357.62 341.532 381.58 300.664 378.95 244.639C372.26 194.314 345.33 159.555 298.14 139.752C300.69 125.278 303.28 110.593 305.96 95.3973C310 96.7796 313.75 97.8414 317.32 99.3439C328.19 103.932 338.89 108.93 348.68 115.541C361.67 124.316 373.44 134.654 383.65 146.473C394.52 159.055 403.39 173.078 409.96 188.434C411.1 191.099 411.3 191.279 413.95 190.578C426.53 187.272 439.11 183.917 452.08 180.481C451.53 178.608 451.18 176.845 450.5 175.232C442.95 157.262 433.28 140.493 421.33 125.107C414.3 116.052 406.71 107.398 398.08 99.8448C385.23 88.6058 371.51 78.599 356.54 70.1948C345.76 64.1446 334.5 59.3665 322.93 55.3798C312.2 51.6836 301.17 48.8788 289.88 46.9756C274.75 44.4313 259.58 43.7201 244.33 44.221C238.75 44.4013 233.21 45.6033 227.63 46.004C218.2 46.6851 209.07 48.979 199.97 51.2128C191.14 53.3764 182.57 56.4516 174.19 60.0276C170.54 61.5903 166.73 62.8324 163.19 64.6053C157.26 67.5804 151.36 70.6455 145.67 74.0713C140.29 77.3068 135.08 80.8628 130.03 84.5991C124.85 88.4255 119.84 92.5124 114.96 96.7195C110.31 100.726 105.67 104.793 101.47 109.261C91.13 120.259 81.6 131.919 74.05 145.061C70.17 151.812 66.21 158.564 62.88 165.596C55.64 180.881 50.13 196.788 46.94 213.466C46.13 217.674 44.91 221.79 44.92 226.148C44.92 227.36 44.13 228.582 43.6 230.084H0C0 229.083 0 228.081 0 227.089C0.32 226.548 0.92 226.008 0.9 225.477C0.77 220.298 2.02 215.33 2.92 210.291C5.03 198.581 7.94 187.072 11.69 175.803C13.88 169.212 16.51 162.771 19.06 156.31C23.14 145.933 28.4 136.146 34.05 126.57C39.44 117.444 45.43 108.74 51.82 100.265C57.72 92.4423 64.04 84.9797 70.83 77.9879C77.05 71.5771 84.16 66.0378 90.65 59.8774C98.64 52.3046 107.78 46.2644 116.92 40.3544C127.06 33.8034 137.58 27.8033 148.82 23.0953C159.82 18.4876 170.77 13.8598 182.24 10.4941C197.31 6.07663 212.68 3.1517 228.23 1.2585C235.77 0.336948 243.45 0.136611 251.06 0.206729C260.84 0.296881 270.65 -0.714826 280.41 1.05816C287.53 2.35034 294.77 2.98141 301.92 4.14337C313.5 6.02654 324.81 9.12177 335.97 12.6778C345.29 15.6428 354.2 19.6796 363.17 23.5661C372.81 27.7432 381.74 33.1222 390.71 38.4312C396.54 41.877 401.86 46.1642 407.55 49.8705C416.62 55.7705 424.49 63.1529 432.47 70.325C439 76.1949 444.72 82.9663 450.72 89.4172C452.86 91.7211 454.91 94.1352 456.73 96.6795C462.01 104.082 467.32 111.474 472.31 119.057C479.9 130.577 486.42 142.737 491.58 155.519C497.71 170.684 503.37 186.06 506.52 202.197C507.75 208.498 509.02 214.799 509.94 221.149C510.6 225.717 510.7 230.365 511.12 234.973C511.16 235.373 511.69 235.724 511.99 236.095V277.084L512 277.074Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_127_331"
|
||||
x1="256"
|
||||
y1="0"
|
||||
x2="256"
|
||||
y2="512"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#05CE45" />
|
||||
<stop offset="0.678759" stop-color="#051F10" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_127_331">
|
||||
<rect width="512" height="512" rx="256" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="overlay">
|
||||
<h2 class="main-header">
|
||||
Read more about <br />
|
||||
<strong class="main-header-strong">Modrinth</strong>
|
||||
</h2>
|
||||
<ButtonStyled color="brand">
|
||||
<a
|
||||
href="https://blog.modrinth.com/?utm_source=website&utm_source=homepage&utm_campaign=newsletter"
|
||||
>
|
||||
<NewspaperIcon />
|
||||
Visit the blog
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { Avatar, ButtonStyled, useRelativeTime } from "@modrinth/ui";
|
||||
import { ButtonStyled, useRelativeTime } from "@modrinth/ui";
|
||||
import {
|
||||
CompassIcon,
|
||||
LogInIcon,
|
||||
DashboardIcon,
|
||||
NewspaperIcon,
|
||||
SearchIcon,
|
||||
CalendarIcon,
|
||||
ModrinthIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ref } from "vue";
|
||||
import PrismLauncherLogo from "~/assets/images/external/prism.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 LatestNewsRow from "~/components/ui/news/LatestNewsRow.vue";
|
||||
|
||||
import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json";
|
||||
|
||||
|
||||
Loaded 100 of 583 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user