mirror of
https://github.com/modrinth/code.git
synced 2026-08-03 06:35:53 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9d3bd1daa | ||
|
|
96545c825b | ||
|
|
ab0d1eceb4 | ||
|
|
f0f012a8fa | ||
|
|
fe9c616591 | ||
|
|
e197db067c | ||
|
|
dd73d06a50 | ||
|
|
916daa684d | ||
|
|
8ece0bd31c | ||
|
|
e2085c43c5 | ||
|
|
30e23e5cc2 | ||
|
|
8e44f20eec | ||
|
|
932858b5f5 | ||
|
|
8b6f0ec695 | ||
|
|
ee2ad5f05b | ||
|
|
9bfa7c7e4a | ||
|
|
e52ae416a8 | ||
|
|
6319832465 | ||
|
|
7144f08f0f | ||
|
|
c579925f03 | ||
|
|
6b569e4ee4 | ||
|
|
6b62e3b480 | ||
|
|
543cb42210 | ||
|
|
86c6fb8239 | ||
|
|
14e0abb273 | ||
|
|
df653d3e43 |
@@ -101,6 +101,10 @@ jobs:
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Ensure sccache directory exists
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
|
||||
@@ -87,7 +87,11 @@ jobs:
|
||||
command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }}
|
||||
|
||||
comment:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
if: |
|
||||
!cancelled()
|
||||
&& github.repository_owner == 'modrinth'
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
&& needs.deploy.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy, deploy-storybook]
|
||||
steps:
|
||||
@@ -101,16 +105,25 @@ jobs:
|
||||
id: urls
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
STORYBOOK_RESULT: ${{ needs.deploy-storybook.result }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt)
|
||||
PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt)
|
||||
|
||||
if [ "$STORYBOOK_RESULT" = "success" ]; then
|
||||
STORYBOOK_PREVIEW_URL="https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev"
|
||||
else
|
||||
STORYBOOK_PREVIEW_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))"
|
||||
fi
|
||||
|
||||
echo "Production preview URL: $PRODUCTION_PREVIEW_URL"
|
||||
echo "Staging preview URL: $STAGING_PREVIEW_URL"
|
||||
echo "Storybook preview URL: $STORYBOOK_PREVIEW_URL"
|
||||
|
||||
echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "storybook-preview-url=https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "storybook-preview-url=$STORYBOOK_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find comment
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
@@ -109,6 +109,10 @@ jobs:
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Ensure sccache directory exists
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
|
||||
Generated
+114
@@ -2133,6 +2133,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||
dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.5"
|
||||
@@ -3259,6 +3268,18 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
@@ -3835,6 +3856,12 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
@@ -4146,6 +4173,29 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dashmap",
|
||||
"futures-sink",
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"getrandom 0.3.3",
|
||||
"hashbrown 0.16.0",
|
||||
"nonzero_ext",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"quanta",
|
||||
"rand 0.9.2",
|
||||
"smallvec",
|
||||
"spinning_top",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
@@ -5440,13 +5490,16 @@ dependencies = [
|
||||
"json-patch 4.1.0",
|
||||
"labrinth",
|
||||
"lettre",
|
||||
"lz4_flex",
|
||||
"meilisearch-sdk",
|
||||
"modrinth-content-management",
|
||||
"modrinth-util",
|
||||
"muralpay",
|
||||
"murmur2",
|
||||
"neverbounce",
|
||||
"paste",
|
||||
"path-util",
|
||||
"postcard",
|
||||
"prometheus",
|
||||
"quick-xml 0.38.3",
|
||||
"rand 0.8.5",
|
||||
@@ -6224,6 +6277,14 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neverbounce"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.24",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
@@ -6268,6 +6329,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
@@ -7492,6 +7559,18 @@ version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
"embedded-io 0.6.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
@@ -7856,6 +7935,21 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quartz_nbt"
|
||||
version = "0.2.9"
|
||||
@@ -8183,6 +8277,15 @@ dependencies = [
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -9794,6 +9897,15 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
|
||||
[[package]]
|
||||
name = "spinning_top"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -10896,8 +11008,10 @@ dependencies = [
|
||||
"fs4",
|
||||
"futures",
|
||||
"futures-lite 2.6.1",
|
||||
"governor",
|
||||
"heck 0.5.0",
|
||||
"hickory-resolver 0.25.2",
|
||||
"httpdate",
|
||||
"indicatif",
|
||||
"itertools 0.14.0",
|
||||
"modrinth-content-management",
|
||||
|
||||
+11
@@ -13,6 +13,7 @@ members = [
|
||||
"packages/modrinth-log",
|
||||
"packages/modrinth-maxmind",
|
||||
"packages/modrinth-util",
|
||||
"packages/neverbounce",
|
||||
"packages/path-util",
|
||||
]
|
||||
|
||||
@@ -88,10 +89,12 @@ fs4 = { version = "0.13.1", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.6.1"
|
||||
futures-util = "0.3.31"
|
||||
governor = "0.10.4"
|
||||
heck = "0.5.0"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25.2"
|
||||
hmac = "0.12.1"
|
||||
httpdate = "1.0.3"
|
||||
hyper = "1.7.0"
|
||||
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||
"aws-lc-rs",
|
||||
@@ -118,6 +121,12 @@ lettre = { version = "0.11.19", default-features = false, features = [
|
||||
"tokio1",
|
||||
"tokio1-rustls",
|
||||
] }
|
||||
lz4_flex = { version = "0.11.5", default-features = false, features = [
|
||||
"checked-decode",
|
||||
"safe-decode",
|
||||
"safe-encode",
|
||||
"std",
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||
modrinth-content-management = { path = "packages/modrinth-content-management" }
|
||||
@@ -126,6 +135,7 @@ modrinth-util = { path = "packages/modrinth-util" }
|
||||
muralpay = { path = "packages/muralpay" }
|
||||
murmur2 = "0.1.0"
|
||||
native-dialog = "0.9.2"
|
||||
neverbounce = { path = "packages/neverbounce" }
|
||||
notify = { version = "8.2.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
@@ -135,6 +145,7 @@ paste = "1.0.15"
|
||||
path-util = { path = "packages/path-util" }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
png = "0.18.0"
|
||||
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
|
||||
proc-macro2 = { version = "1.0" }
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
|
||||
@@ -8,8 +8,11 @@ extend-exclude = [
|
||||
"packages/blog/",
|
||||
# contains licenses like `CC-BY-ND-4.0`
|
||||
"packages/moderation/src/data/stages/license.ts",
|
||||
"packages/moderation/src/utils.ts",
|
||||
# contains payment card IDs like `IY1VMST1MOXS` which are flagged
|
||||
"apps/labrinth/src/queue/payouts/mod.rs",
|
||||
# contains domain names with deliberate typos
|
||||
"apps/labrinth/assets/disposable_email_blocklist.txt"
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
|
||||
+114
-21
@@ -89,7 +89,14 @@ import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import WindowControls from '@/components/ui/WindowControls.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import { config } from '@/config'
|
||||
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import {
|
||||
ads_consent_listener,
|
||||
get_ads_consent_required,
|
||||
hide_ads_window,
|
||||
init_ads_window,
|
||||
perform_ads_consent_action,
|
||||
show_ads_window,
|
||||
} from '@/helpers/ads.js'
|
||||
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user, get_version } from '@/helpers/cache.js'
|
||||
@@ -184,6 +191,8 @@ const { handleError, addNotification } = notificationManager
|
||||
const popupNotificationManager = new AppPopupNotificationManager()
|
||||
providePopupNotificationManager(popupNotificationManager)
|
||||
const { addPopupNotification } = popupNotificationManager
|
||||
let adsConsentPopupId = null
|
||||
let unlistenAdsConsent
|
||||
|
||||
const appVersion = getVersion()
|
||||
const tauriApiClient = new TauriModrinthClient({
|
||||
@@ -213,9 +222,20 @@ const { data: authenticatedModrinthUser } = useQuery({
|
||||
enabled: () => !!credentials.value?.session,
|
||||
retry: false,
|
||||
})
|
||||
const hasPlus = computed(
|
||||
() =>
|
||||
!!credentials.value?.user &&
|
||||
(hasMidasBadge(credentials.value.user) ||
|
||||
hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)),
|
||||
)
|
||||
const showAd = computed(
|
||||
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
|
||||
)
|
||||
const adConsentAvailable = computed(() => credentials.value !== undefined && !hasPlus.value)
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(true),
|
||||
showAds: ref(false),
|
||||
showAds: showAd,
|
||||
adConsentAvailable,
|
||||
floatingActionBarOffsets: {
|
||||
left: ref(APP_LEFT_NAV_WIDTH),
|
||||
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
|
||||
@@ -295,6 +315,12 @@ const authUnreachable = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
await useCheckDisableMouseover()
|
||||
try {
|
||||
unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired)
|
||||
handleAdsConsentRequired(await get_ads_consent_required())
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
document.querySelector('body').addEventListener('click', handleClick)
|
||||
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
||||
@@ -308,6 +334,7 @@ onUnmounted(async () => {
|
||||
unsubscribeSidebarToggle()
|
||||
clearDelayedUpdatePopup()
|
||||
|
||||
await unlistenAdsConsent?.()
|
||||
await unlistenUpdateDownload?.()
|
||||
})
|
||||
|
||||
@@ -332,8 +359,77 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
'Minecraft authentication servers may be down right now. Check your internet connection and try again later.',
|
||||
},
|
||||
adsConsentTitle: {
|
||||
id: 'app.ads-consent.title',
|
||||
defaultMessage: 'Your privacy and how ads support Modrinth',
|
||||
},
|
||||
adsConsentBody: {
|
||||
id: 'app.ads-consent.body',
|
||||
defaultMessage:
|
||||
'Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance.',
|
||||
},
|
||||
adsConsentManage: {
|
||||
id: 'app.ads-consent.manage',
|
||||
defaultMessage: 'Manage preferences',
|
||||
},
|
||||
adsConsentReject: {
|
||||
id: 'app.ads-consent.reject',
|
||||
defaultMessage: 'Reject all',
|
||||
},
|
||||
adsConsentAccept: {
|
||||
id: 'app.ads-consent.accept',
|
||||
defaultMessage: 'Accept all',
|
||||
},
|
||||
})
|
||||
|
||||
function handleAdsConsentRequired(required) {
|
||||
if (!required) {
|
||||
if (adsConsentPopupId !== null) {
|
||||
popupNotificationManager.removeNotification(adsConsentPopupId)
|
||||
adsConsentPopupId = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
adsConsentPopupId !== null &&
|
||||
popupNotificationManager.getNotifications().some((item) => item.id === adsConsentPopupId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const notification = addPopupNotification({
|
||||
title: formatMessage(messages.adsConsentTitle),
|
||||
text: formatMessage(messages.adsConsentBody),
|
||||
type: 'info',
|
||||
hideIcon: true,
|
||||
autoCloseMs: null,
|
||||
dismissible: false,
|
||||
buttons: [
|
||||
{
|
||||
label: formatMessage(messages.adsConsentManage),
|
||||
action: () => perform_ads_consent_action('manage').catch(handleError),
|
||||
color: 'standard',
|
||||
keepOpen: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.adsConsentReject),
|
||||
action: () => perform_ads_consent_action('reject').catch(handleError),
|
||||
color: 'brand',
|
||||
keepOpen: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.adsConsentAccept),
|
||||
action: () => perform_ads_consent_action('accept').catch(handleError),
|
||||
color: 'brand',
|
||||
keepOpen: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
adsConsentPopupId = notification.id
|
||||
}
|
||||
|
||||
async function setupApp() {
|
||||
const {
|
||||
native_decorations,
|
||||
@@ -403,7 +499,7 @@ async function setupApp() {
|
||||
addNotification({
|
||||
title: 'Warning',
|
||||
text: e.message,
|
||||
type: 'warn',
|
||||
type: 'warning',
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -703,17 +799,6 @@ async function logOut() {
|
||||
await fetchCredentials()
|
||||
}
|
||||
|
||||
const hasPlus = computed(
|
||||
() =>
|
||||
!!credentials.value?.user &&
|
||||
(hasMidasBadge(credentials.value.user) ||
|
||||
hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)),
|
||||
)
|
||||
|
||||
const showAd = computed(
|
||||
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
|
||||
)
|
||||
|
||||
async function fetchIntercomToken() {
|
||||
const creds = await getCreds()
|
||||
if (!creds?.session) {
|
||||
@@ -740,13 +825,21 @@ async function fetchIntercomToken() {
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
watch(showAd, () => {
|
||||
if (!showAd.value) {
|
||||
hide_ads_window(true)
|
||||
} else {
|
||||
init_ads_window(true)
|
||||
}
|
||||
})
|
||||
watch(
|
||||
[showAd, adConsentAvailable],
|
||||
async ([showAds, canManageConsent]) => {
|
||||
if (showAds) {
|
||||
await init_ads_window(true)
|
||||
return
|
||||
}
|
||||
|
||||
await hide_ads_window(true)
|
||||
if (canManageConsent) {
|
||||
await init_ads_window()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
invoke('show_window')
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<PageHeader :title="instance.name">
|
||||
<template #leading>
|
||||
<Avatar :src="iconSrc" :alt="instance.name" size="64px" :tint-by="instance.id" />
|
||||
</template>
|
||||
|
||||
<template #metadata>
|
||||
<InstanceHeaderServerMetadata
|
||||
v-if="isServerInstance"
|
||||
:loading-server-ping="loadingServerPing"
|
||||
:players-online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
:recent-plays="recentPlays"
|
||||
:ping="ping"
|
||||
:minecraft-server="minecraftServer"
|
||||
:linked-project-v3="linkedProjectV3"
|
||||
:instance-id="instance.id"
|
||||
/>
|
||||
<PageHeaderMetadata v-else>
|
||||
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">
|
||||
Minecraft {{ instance.game_version }}
|
||||
</PageHeaderMetadataItem>
|
||||
<PageHeaderMetadataItem
|
||||
:icon="ServerLoaderIcon"
|
||||
:icon-props="{ loader: loaderDisplayName }"
|
||||
tooltip="Mod loader"
|
||||
>
|
||||
{{ loaderLabel }}
|
||||
</PageHeaderMetadataItem>
|
||||
<PageHeaderMetadataItem
|
||||
v-if="showInstancePlayTime"
|
||||
:icon="TimerIcon"
|
||||
tooltip="Total playtime"
|
||||
>
|
||||
{{ playtimeLabel }}
|
||||
</PageHeaderMetadataItem>
|
||||
</PageHeaderMetadata>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<ButtonStyled v-if="isInstalling" color="brand" size="large">
|
||||
<button type="button" disabled>
|
||||
{{ formatMessage(commonMessages.installingLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="instance.install_stage !== 'installed'" color="brand" size="large">
|
||||
<button type="button" @click="emit('repair')">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.repair) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing" color="red" size="large">
|
||||
<button type="button" :disabled="stopping" @click="emit('stop')">
|
||||
<StopCircleIcon />
|
||||
{{
|
||||
stopping ? formatMessage(messages.stopping) : formatMessage(commonMessages.stopButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<JoinedButtons
|
||||
v-else-if="!loading && isServerInstance"
|
||||
:actions="serverPlayActions"
|
||||
color="brand"
|
||||
size="large"
|
||||
/>
|
||||
<ButtonStyled v-else-if="!loading" color="brand" size="large">
|
||||
<button type="button" @click="emit('play')">
|
||||
<PlayIcon />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="brand" size="large">
|
||||
<button type="button" disabled>{{ formatMessage(messages.starting) }}</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled circular size="large">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.instanceSettings)"
|
||||
type="button"
|
||||
:aria-label="formatMessage(messages.instanceSettings)"
|
||||
@click="emit('settings')"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="moreActions"
|
||||
:tooltip="formatMessage(messages.moreActions)"
|
||||
:aria-label="formatMessage(messages.moreActions)"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</PageHeaderActions>
|
||||
</template>
|
||||
</PageHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
DownloadIcon,
|
||||
ExternalIcon,
|
||||
FolderOpenIcon,
|
||||
MoreVerticalIcon,
|
||||
PackageIcon,
|
||||
PlayIcon,
|
||||
SettingsIcon,
|
||||
StopCircleIcon,
|
||||
TagCategoryGamepad2Icon as Gamepad2Icon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
formatLoaderLabel,
|
||||
type JoinedButtonAction,
|
||||
JoinedButtons,
|
||||
LoaderIcon as ServerLoaderIcon,
|
||||
PageHeader,
|
||||
PageHeaderActions,
|
||||
PageHeaderMetadata,
|
||||
PageHeaderMetadataItem,
|
||||
type ServerLoader,
|
||||
TeleportOverflowMenu,
|
||||
type TeleportOverflowMenuItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
import InstanceHeaderServerMetadata from './instance-page-header-server-metadata.vue'
|
||||
|
||||
const messages = defineMessages({
|
||||
createShortcut: {
|
||||
id: 'instance.action.create-shortcut',
|
||||
defaultMessage: 'Create shortcut',
|
||||
},
|
||||
exportModpack: {
|
||||
id: 'instance.action.export-modpack',
|
||||
defaultMessage: 'Export modpack',
|
||||
},
|
||||
instanceSettings: {
|
||||
id: 'instance.action.settings',
|
||||
defaultMessage: 'Instance settings',
|
||||
},
|
||||
launchInstance: {
|
||||
id: 'instance.action.launch-instance',
|
||||
defaultMessage: 'Launch instance',
|
||||
},
|
||||
moreActions: {
|
||||
id: 'instance.action.more-actions',
|
||||
defaultMessage: 'More actions',
|
||||
},
|
||||
neverPlayed: {
|
||||
id: 'instance.playtime.never-played',
|
||||
defaultMessage: 'Never played',
|
||||
},
|
||||
openFolder: {
|
||||
id: 'instance.action.open-folder',
|
||||
defaultMessage: 'Open folder',
|
||||
},
|
||||
repair: {
|
||||
id: 'instance.action.repair',
|
||||
defaultMessage: 'Repair',
|
||||
},
|
||||
starting: {
|
||||
id: 'instance.action.starting',
|
||||
defaultMessage: 'Starting...',
|
||||
},
|
||||
stopping: {
|
||||
id: 'instance.action.stopping',
|
||||
defaultMessage: 'Stopping...',
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
instance: GameInstance
|
||||
iconSrc?: string | null
|
||||
isServerInstance?: boolean
|
||||
showInstancePlayTime?: boolean
|
||||
timePlayed?: number
|
||||
playing?: boolean
|
||||
loading?: boolean
|
||||
stopping?: boolean
|
||||
loadingServerPing?: boolean
|
||||
playersOnline?: number
|
||||
statusOnline?: boolean
|
||||
recentPlays?: number
|
||||
ping?: number
|
||||
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
|
||||
linkedProjectV3?: Labrinth.Projects.v3.Project
|
||||
}>(),
|
||||
{
|
||||
iconSrc: null,
|
||||
isServerInstance: false,
|
||||
showInstancePlayTime: false,
|
||||
timePlayed: 0,
|
||||
playing: false,
|
||||
loading: false,
|
||||
stopping: false,
|
||||
loadingServerPing: false,
|
||||
playersOnline: undefined,
|
||||
statusOnline: false,
|
||||
recentPlays: undefined,
|
||||
ping: undefined,
|
||||
minecraftServer: undefined,
|
||||
linkedProjectV3: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
repair: []
|
||||
stop: []
|
||||
play: []
|
||||
playServer: []
|
||||
settings: []
|
||||
openFolder: []
|
||||
export: []
|
||||
createShortcut: []
|
||||
}>()
|
||||
|
||||
const installingStages = [
|
||||
'installing',
|
||||
'pack_installing',
|
||||
'pack_installed',
|
||||
'not_installed',
|
||||
'minecraft_installing',
|
||||
]
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const isInstalling = computed(() => installingStages.includes(props.instance.install_stage))
|
||||
const loaderDisplayName = computed(() => formatLoaderLabel(props.instance.loader) as ServerLoader)
|
||||
const loaderLabel = computed(() =>
|
||||
[loaderDisplayName.value, props.instance.loader_version].filter(Boolean).join(' '),
|
||||
)
|
||||
const playtimeLabel = computed(() => {
|
||||
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
|
||||
|
||||
const hours = Math.floor(props.timePlayed / 3600)
|
||||
if (hours >= 1) {
|
||||
return `${hours} hour${hours > 1 ? 's' : ''}`
|
||||
}
|
||||
|
||||
const minutes = Math.floor(props.timePlayed / 60)
|
||||
if (minutes >= 1) {
|
||||
return `${minutes} minute${minutes > 1 ? 's' : ''}`
|
||||
}
|
||||
|
||||
const seconds = Math.floor(props.timePlayed)
|
||||
return `${seconds} second${seconds > 1 ? 's' : ''}`
|
||||
})
|
||||
const serverPlayActions = computed<JoinedButtonAction[]>(() => [
|
||||
{
|
||||
id: 'join_server',
|
||||
label: formatMessage(commonMessages.playButton),
|
||||
icon: PlayIcon,
|
||||
action: () => emit('playServer'),
|
||||
},
|
||||
{
|
||||
id: 'launch_instance',
|
||||
label: formatMessage(messages.launchInstance),
|
||||
icon: PlayIcon,
|
||||
action: () => emit('play'),
|
||||
},
|
||||
])
|
||||
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: formatMessage(messages.openFolder),
|
||||
icon: FolderOpenIcon,
|
||||
action: () => emit('openFolder'),
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
label: formatMessage(messages.exportModpack),
|
||||
icon: PackageIcon,
|
||||
action: () => emit('export'),
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
label: formatMessage(messages.createShortcut),
|
||||
icon: ExternalIcon,
|
||||
action: () => emit('createShortcut'),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<template v-if="loadingServerPing">
|
||||
<ServerOnlinePlayers
|
||||
v-if="playersOnline !== undefined"
|
||||
:online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
hide-label
|
||||
/>
|
||||
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
||||
<div
|
||||
v-if="
|
||||
(playersOnline !== undefined || recentPlays !== undefined) &&
|
||||
(minecraftServer?.region || ping)
|
||||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
<ServerPing v-if="ping" :ping="ping" />
|
||||
</template>
|
||||
|
||||
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
|
||||
|
||||
<div v-if="minecraftServer?.region || ping" class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
|
||||
|
||||
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
|
||||
Linked to
|
||||
<Avatar
|
||||
:src="linkedProjectV3.icon_url"
|
||||
:alt="linkedProjectV3.name"
|
||||
:tint-by="instanceId"
|
||||
size="24px"
|
||||
/>
|
||||
<router-link
|
||||
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
|
||||
class="hover:underline text-primary truncate"
|
||||
>
|
||||
{{ linkedProjectV3.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
Avatar,
|
||||
ServerOnlinePlayers,
|
||||
ServerPing,
|
||||
ServerRecentPlays,
|
||||
ServerRegion,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
defineProps<{
|
||||
loadingServerPing?: boolean
|
||||
playersOnline?: number
|
||||
statusOnline?: boolean
|
||||
recentPlays?: number
|
||||
ping?: number
|
||||
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
|
||||
linkedProjectV3?: Labrinth.Projects.v3.Project
|
||||
instanceId?: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,12 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { Settings2Icon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
Toggle,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { open_ads_consent_preferences } from '@/helpers/ads.js'
|
||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { adConsentAvailable } = injectPageContext()
|
||||
const settings = ref(await get())
|
||||
|
||||
const messages = defineMessages({
|
||||
adsConsentTitle: {
|
||||
id: 'app.ads-consent.title',
|
||||
defaultMessage: 'Your privacy and how ads support Modrinth',
|
||||
},
|
||||
adsConsentIntro: {
|
||||
id: 'app.settings.privacy.ads-consent.intro',
|
||||
defaultMessage:
|
||||
'Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance. You can opt out or manage your preferences below.',
|
||||
},
|
||||
adsConsentManage: {
|
||||
id: 'app.ads-consent.manage',
|
||||
defaultMessage: 'Manage preferences',
|
||||
},
|
||||
})
|
||||
|
||||
async function manageAdsPreferences() {
|
||||
await open_ads_consent_preferences().catch(handleError)
|
||||
}
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
@@ -23,18 +55,26 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Personalized ads</h2>
|
||||
<p class="m-0 mt-1 text-sm">
|
||||
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||
option, you opt out and ads will no longer be shown based on your interests.
|
||||
</p>
|
||||
<div v-if="adConsentAvailable">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.adsConsentTitle) }}
|
||||
</h2>
|
||||
<div class="mt-1 flex flex-col gap-2.5 items-start">
|
||||
<div class="flex flex-col gap-1 items-start">
|
||||
<div class="text-sm">
|
||||
{{ formatMessage(messages.adsConsentIntro) }}
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" @click="manageAdsPreferences">
|
||||
<Settings2Icon aria-hidden="true" />
|
||||
{{ formatMessage(messages.adsConsentManage) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<Toggle id="personalized-ads" v-model="settings.personalized_ads" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div class="mt-8 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Telemetry</h2>
|
||||
<p class="m-0 mt-1 text-sm">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<SkinPreviewRenderer
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin || ''"
|
||||
:ears-texture-src="uploadedTextureUrl?.original ?? currentSkin?.texture"
|
||||
:cape-src="selectedCapeTexture"
|
||||
framing="modal"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
export async function init_ads_window(overrideShown = false) {
|
||||
return await invoke('plugin:ads|init_ads_window', {
|
||||
@@ -15,6 +16,22 @@ export async function hide_ads_window(reset) {
|
||||
return await invoke('plugin:ads|hide_ads_window', { reset })
|
||||
}
|
||||
|
||||
export async function get_ads_consent_required() {
|
||||
return await invoke('plugin:ads|get_ads_consent_required')
|
||||
}
|
||||
|
||||
export async function perform_ads_consent_action(action) {
|
||||
return await invoke('plugin:ads|perform_ads_consent_action', { action })
|
||||
}
|
||||
|
||||
export async function open_ads_consent_preferences() {
|
||||
return await invoke('plugin:ads|open_ads_consent_preferences')
|
||||
}
|
||||
|
||||
export async function ads_consent_listener(callback) {
|
||||
return await listen('ads-consent-required', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function record_ads_click() {
|
||||
return await invoke('plugin:ads|record_ads_click')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
|
||||
export async function get_project(id, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_project', { id, cacheBehaviour })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { User } from '@modrinth/utils'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
|
||||
import { install_job_listener } from './events'
|
||||
import type { InstanceLink, InstanceLoader } from './types'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { ContentItem, ContentOwner } from '@modrinth/ui'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
|
||||
import type { InstallJobSnapshot } from './install'
|
||||
import type {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
|
||||
|
||||
export type OperationCause =
|
||||
| 'app/startup'
|
||||
| 'navigation/home'
|
||||
| 'navigation/library'
|
||||
| 'navigation/browse'
|
||||
| 'navigation/project'
|
||||
| 'navigation/instance/overview'
|
||||
| 'navigation/instance/content'
|
||||
| 'navigation/instance/logs'
|
||||
| 'navigation/servers'
|
||||
| 'navigation/server/manage'
|
||||
| 'navigation/server/content'
|
||||
| 'instance/refresh/user'
|
||||
| 'instance/refresh/filesystem_watch'
|
||||
| 'instance/update/all'
|
||||
| 'instance/update/single'
|
||||
| 'instance/install'
|
||||
| 'cache/revalidate'
|
||||
| 'auth/session_refresh'
|
||||
| 'background/friends'
|
||||
| 'minecraft/launch'
|
||||
| 'app/update_check'
|
||||
| 'unattributed'
|
||||
|
||||
function navigationCause(pathname: string): OperationCause {
|
||||
if (pathname === '/hosting/manage' || pathname === '/hosting/manage/') {
|
||||
return 'navigation/servers'
|
||||
}
|
||||
if (pathname.startsWith('/hosting/manage/')) {
|
||||
return pathname.split('/').includes('content')
|
||||
? 'navigation/server/content'
|
||||
: 'navigation/server/manage'
|
||||
}
|
||||
if (pathname.startsWith('/browse/')) return 'navigation/browse'
|
||||
if (pathname.startsWith('/project/')) return 'navigation/project'
|
||||
if (pathname.startsWith('/library')) return 'navigation/library'
|
||||
if (pathname.startsWith('/instance/')) {
|
||||
return pathname.split('/').includes('logs')
|
||||
? 'navigation/instance/logs'
|
||||
: 'navigation/instance/content'
|
||||
}
|
||||
|
||||
return 'navigation/home'
|
||||
}
|
||||
|
||||
function commandCause(command: string): OperationCause {
|
||||
if (command === 'plugin:instance|instance_update_all') return 'instance/update/all'
|
||||
if (
|
||||
command === 'plugin:instance|instance_update_project' ||
|
||||
command === 'plugin:instance|instance_switch_project_version_with_dependencies' ||
|
||||
command === 'plugin:instance|instance_update_managed_modrinth_version'
|
||||
) {
|
||||
return 'instance/update/single'
|
||||
}
|
||||
if (
|
||||
command.startsWith('plugin:install|install_') ||
|
||||
command === 'plugin:jre|jre_auto_install_java' ||
|
||||
command === 'plugin:instance|instance_add_project_from_version' ||
|
||||
command === 'plugin:instance|instance_install_project_with_dependencies' ||
|
||||
command === 'plugin:instance|instance_repair_managed_modrinth'
|
||||
) {
|
||||
return 'instance/install'
|
||||
}
|
||||
if (
|
||||
command === 'plugin:instance|instance_run' ||
|
||||
command === 'plugin:worlds|start_join_singleplayer_world' ||
|
||||
command === 'plugin:worlds|start_join_server'
|
||||
) {
|
||||
return 'minecraft/launch'
|
||||
}
|
||||
if (command === 'plugin:mr-auth|get') return 'auth/session_refresh'
|
||||
|
||||
return navigationCause(window.location.pathname)
|
||||
}
|
||||
|
||||
export function invoke<T>(command: string, args: Record<string, unknown> = {}): Promise<T> {
|
||||
return tauriInvoke<T>(command, {
|
||||
...args,
|
||||
invocationContext: { cause: commandCause(command) },
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
|
||||
/*
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
|
||||
/// Gets the game versions from daedalus
|
||||
// Returns a VersionManifest
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
|
||||
export type ModrinthCredentials = {
|
||||
session: string
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
import {
|
||||
applyCapeTexture,
|
||||
applyEarsMod,
|
||||
createTransparentTexture,
|
||||
disposeCaches,
|
||||
loadTexture,
|
||||
removeEarsMod,
|
||||
setupSkinModel,
|
||||
} from '@modrinth/ui'
|
||||
import * as THREE from 'three'
|
||||
@@ -71,12 +74,13 @@ class BatchSkinRenderer {
|
||||
textureUrl: string,
|
||||
modelUrl: string,
|
||||
capeUrl?: string,
|
||||
earsTextureUrl?: string,
|
||||
): Promise<RawRenderResult> {
|
||||
this.initializeRenderer()
|
||||
|
||||
this.clearScene()
|
||||
|
||||
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
||||
await this.setupModel(modelUrl, textureUrl, capeUrl, earsTextureUrl)
|
||||
|
||||
const headPart = this.currentModel!.getObjectByName('Head')
|
||||
let lookAtTarget: [number, number, number]
|
||||
@@ -123,17 +127,29 @@ class BatchSkinRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||
private async setupModel(
|
||||
modelUrl: string,
|
||||
textureUrl: string,
|
||||
capeUrl?: string,
|
||||
earsTextureUrl?: string,
|
||||
): Promise<void> {
|
||||
if (!this.scene) {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl, capeUrl)
|
||||
const [{ model }, earsTexture] = await Promise.all([
|
||||
setupSkinModel(modelUrl, textureUrl, capeUrl),
|
||||
earsTextureUrl ? loadTexture(earsTextureUrl) : Promise.resolve(null),
|
||||
])
|
||||
|
||||
if (!capeUrl) {
|
||||
applyCapeTexture(model, null, this.getTransparentTexture())
|
||||
}
|
||||
|
||||
if (earsTexture) {
|
||||
applyEarsMod(model, earsTexture)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.add(model)
|
||||
group.position.set(0, 0.3, 1.95)
|
||||
@@ -154,6 +170,7 @@ class BatchSkinRenderer {
|
||||
private clearScene(): void {
|
||||
if (!this.scene || !this.currentModel) return
|
||||
|
||||
removeEarsMod(this.currentModel)
|
||||
this.scene.remove(this.currentModel)
|
||||
this.currentModel.clear()
|
||||
this.currentModel = null
|
||||
@@ -193,6 +210,7 @@ function getModelUrlForVariant(variant: string): string {
|
||||
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||
export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||
const DEBUG_MODE = false
|
||||
const SKIN_PREVIEW_RENDER_VERSION = 'ears-1'
|
||||
|
||||
let sharedRenderer: BatchSkinRenderer | null = null
|
||||
let latestPreviewGeneration = 0
|
||||
@@ -205,6 +223,10 @@ function getSharedRenderer(): BatchSkinRenderer {
|
||||
return sharedRenderer
|
||||
}
|
||||
|
||||
export function getSkinPreviewKey(skin: Skin): string {
|
||||
return `${SKIN_PREVIEW_RENDER_VERSION}+${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
}
|
||||
|
||||
export function disposeSharedRenderer(): void {
|
||||
if (sharedRenderer) {
|
||||
sharedRenderer.dispose()
|
||||
@@ -217,7 +239,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
const validHeadKeys = new Set<string>()
|
||||
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
const key = getSkinPreviewKey(skin)
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
validKeys.add(key)
|
||||
validHeadKeys.add(headKey)
|
||||
@@ -382,9 +404,7 @@ async function generateSkinPreviewsForGeneration(
|
||||
const isCurrentGeneration = () => generation === latestPreviewGeneration
|
||||
|
||||
try {
|
||||
const skinKeys = skins.map(
|
||||
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
||||
)
|
||||
const skinKeys = skins.map(getSkinPreviewKey)
|
||||
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
|
||||
|
||||
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
|
||||
@@ -415,7 +435,7 @@ async function generateSkinPreviewsForGeneration(
|
||||
for (const skin of skins) {
|
||||
if (!isCurrentGeneration()) return
|
||||
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
const key = getSkinPreviewKey(skin)
|
||||
|
||||
if (skinBlobUrlMap.has(key)) {
|
||||
if (DEBUG_MODE) {
|
||||
@@ -443,6 +463,7 @@ async function generateSkinPreviewsForGeneration(
|
||||
await get_normalized_skin_texture(skin),
|
||||
modelUrl,
|
||||
cape?.texture,
|
||||
skin.texture,
|
||||
)
|
||||
|
||||
if (!isCurrentGeneration()) return
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
|
||||
// Gets cached category tags
|
||||
export async function get_categories() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GameVersion } from '@modrinth/ui'
|
||||
import { autoToHTML } from '@sfirew/minecraft-motd-parser'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invoke } from '@/helpers/invoke'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { get_full_path } from '@/helpers/instance'
|
||||
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Durchsuche {count} Welten..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Nicht Installieren"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installieren zum Spielen"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Trotzdem Installieren"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installieren"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Diese Einladung wurde von einem anderen Modrinth-Nutzer erstellt, nicht von Modrinth. Nimm nur Einladungen von Personen an, denen du vertraust."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# Mod} other {# Mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth veröffentlicht wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Geteilte Instanz"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Dieses Server-Modpack enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Warnung vor unbekannten Dateien"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Nicht erkannte Dateien"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Inhalte ansehen"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Aktualisieren zum Spielen"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Entfernt"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Eine aktualisierung zum spielen von {name} ist benötigt. Bitte aktualisiere auf die neuste Version um das Spiel zu starten."
|
||||
},
|
||||
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Durchsuche {count} Welten..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Nicht Installieren"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installieren zum Spielen"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Trotzdem Installieren"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installieren"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Diese Einladung wurde von einem anderen Modrinth-Nutzer erstellt, nicht von Modrinth. Nimm nur Einladungen von Personen an, denen du vertraust."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# Mod} other {# Mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth veröffentlicht wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Geteilte Instanz"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Dieses Server-Modpack enthält Dateien, die nicht auf Modrinth veröffentlicht sind. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Warnung vor unbekannten Dateien"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Nicht erkannte Dateien"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Inhalte ansehen"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Aktualisieren zum Spielen"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Entfernt"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Zum Spielen von {name} ist eine Aktualisierung erforderlich. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
|
||||
},
|
||||
|
||||
@@ -125,6 +125,21 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "View logs"
|
||||
},
|
||||
"app.ads-consent.accept": {
|
||||
"message": "Accept all"
|
||||
},
|
||||
"app.ads-consent.body": {
|
||||
"message": "Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance."
|
||||
},
|
||||
"app.ads-consent.manage": {
|
||||
"message": "Manage preferences"
|
||||
},
|
||||
"app.ads-consent.reject": {
|
||||
"message": "Reject all"
|
||||
},
|
||||
"app.ads-consent.title": {
|
||||
"message": "Your privacy and how ads support Modrinth"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering."
|
||||
},
|
||||
@@ -500,6 +515,9 @@
|
||||
"app.settings.downloading": {
|
||||
"message": "Downloading v{version}"
|
||||
},
|
||||
"app.settings.privacy.ads-consent.intro": {
|
||||
"message": "Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance. You can opt out or manage your preferences below."
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Appearance"
|
||||
},
|
||||
@@ -542,6 +560,9 @@
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Error processing file"
|
||||
},
|
||||
"app.skins.ears-feature-notice": {
|
||||
"message": "This skin uses features from the {ears} mod"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Edit skin"
|
||||
},
|
||||
@@ -668,6 +689,12 @@
|
||||
"app.skins.title": {
|
||||
"message": "Skin selector"
|
||||
},
|
||||
"app.skins.toggle-ears-features-off": {
|
||||
"message": "Toggle off"
|
||||
},
|
||||
"app.skins.toggle-ears-features-on": {
|
||||
"message": "Toggle on"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App."
|
||||
},
|
||||
@@ -779,6 +806,33 @@
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!"
|
||||
},
|
||||
"instance.action.create-shortcut": {
|
||||
"message": "Create shortcut"
|
||||
},
|
||||
"instance.action.export-modpack": {
|
||||
"message": "Export modpack"
|
||||
},
|
||||
"instance.action.launch-instance": {
|
||||
"message": "Launch instance"
|
||||
},
|
||||
"instance.action.more-actions": {
|
||||
"message": "More actions"
|
||||
},
|
||||
"instance.action.open-folder": {
|
||||
"message": "Open folder"
|
||||
},
|
||||
"instance.action.repair": {
|
||||
"message": "Repair"
|
||||
},
|
||||
"instance.action.settings": {
|
||||
"message": "Instance settings"
|
||||
},
|
||||
"instance.action.starting": {
|
||||
"message": "Starting..."
|
||||
},
|
||||
"instance.action.stopping": {
|
||||
"message": "Stopping..."
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Add and play"
|
||||
},
|
||||
@@ -821,6 +875,9 @@
|
||||
"instance.files.save-as": {
|
||||
"message": "Save as..."
|
||||
},
|
||||
"instance.playtime.never-played": {
|
||||
"message": "Never played"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Address"
|
||||
},
|
||||
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Buscar en {count} mundos..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "No instalar"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Instalar para jugar"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Instalar de todos modos"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Instalar"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Esta invitación la creó otro usuario de Modrinth, no Modrinth. Solo acepta invitaciones de personas en las que confíes."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# mod} other {# mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Un archivo solo se revisa si se publica en Modrinth, sin importar su formato (incluido el .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Instancia compartida"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Este modpack para servidor contiene archivos que no están publicados en Modrinth. Recomendamos encarecidamente instalar únicamente archivos de fuentes de confianza."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Advertencia sobre archivos desconocidos"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Archivos no reconocidos"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Ver contenidos"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Actualizar para jugar"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Eliminado"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Se requiere una actualización para jugar {name}. Por favor, actualiza a la versión más reciente para iniciar el juego."
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"message": "Téléchargements"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Masquer plus d'instances en exécution"
|
||||
"message": "Masquer plus d'instances en cours d'exécution"
|
||||
},
|
||||
"app.action-bar.install.copied-details": {
|
||||
"message": "Copié"
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Annulé"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "Le nettoyage n'est pas terminé"
|
||||
"message": "Le nettoyage n'a pas terminé"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "Impossible de télécharger les fichiers"
|
||||
@@ -65,6 +65,27 @@
|
||||
"app.action-bar.install.summary.java-setup-failed": {
|
||||
"message": "L'installation de Java n'a pas pu se terminer"
|
||||
},
|
||||
"app.action-bar.install.summary.loader-setup-failed": {
|
||||
"message": "La mise en place du loader a échoué"
|
||||
},
|
||||
"app.action-bar.install.summary.local-data-error": {
|
||||
"message": "Impossible de mettre à jour les données locales"
|
||||
},
|
||||
"app.action-bar.install.summary.minecraft-setup-failed": {
|
||||
"message": "La mise en place de Minecraft a échoué"
|
||||
},
|
||||
"app.action-bar.install.summary.modrinth-unreachable": {
|
||||
"message": "Impossible d'atteindre les serveurs Modrinth"
|
||||
},
|
||||
"app.action-bar.install.summary.no-write-permission": {
|
||||
"message": "Aucune permission d'écriture"
|
||||
},
|
||||
"app.action-bar.install.summary.pack-download-failed": {
|
||||
"message": "Impossible de télécharger le pack"
|
||||
},
|
||||
"app.action-bar.install.summary.unexpected-error": {
|
||||
"message": "Quelque chose s'est mal passé"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Instance inconnue"
|
||||
},
|
||||
@@ -72,22 +93,22 @@
|
||||
"message": "Installations"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Définir en tante qu'instance première"
|
||||
"message": "Définir en tant qu'instance principale"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Aucune instance en exécution"
|
||||
"message": "Aucune instance en cours d'exécution"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Hors ligne"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instance première"
|
||||
"message": "Instance principale"
|
||||
},
|
||||
"app.action-bar.reload-to-update": {
|
||||
"message": "Recharger pour mettre à jour"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Montrer plus d'instances en exécution"
|
||||
"message": "Montrer plus d'instances en cours d'exécution"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Arrêter l'instance"
|
||||
@@ -263,6 +284,9 @@
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Finalisation"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "En attente d'installation"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Préparation de Java"
|
||||
},
|
||||
@@ -401,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Rechercher {count} mondes..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Ne pas installer"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installer pour jouer"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Installer quand même"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installer"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Cette invitation a été crée par un autre utilisateur Modrinth, pas par Modrinth. N'acceptez seulement les invitations de personnes en qui vous avez confiance."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# mod} other {# mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Un fichier est seulement examiné s'il est publié sur Modrinth, peu importe son extension (y compris .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Instance partagée"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Ce modpack de serveur contient des fichiers qui ne sont pas publiés sur Modrinth. Nous recommandons fortement de seulement installer des fichiers de sources en qui vous avez confiance."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Avertissement de fichiers inconnus"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Fichiers non reconnus"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Voir le contenu"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Mettre à jour pour jouer"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Supprimé"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Une mise à jour est requise pour jouer à {name}. Veuillez mettre à jour à la dernière version pour lancer le jeu."
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"message": "A letöltés nem sikerült"
|
||||
},
|
||||
"app.action-bar.install.summary.instance-not-found": {
|
||||
"message": "A példány nem található"
|
||||
"message": "A játékprofil nem található"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-file-path": {
|
||||
"message": "A fájl elérési útja érvénytelen"
|
||||
@@ -87,7 +87,7 @@
|
||||
"message": "Valami hiba történt"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Ismeretlen profil"
|
||||
"message": "Ismeretlen játékprofil"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Telepítések"
|
||||
@@ -240,7 +240,7 @@
|
||||
"message": "Modcsomagok"
|
||||
},
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "A tartalom hozzáadása ronthatja a kompatibilitást a szerverhez való csatlakozáskor. A hozzáadott tartalom elveszik, ha frissíted a szerverpéldány tartalmát."
|
||||
"message": "A tartalom hozzáadása ronthatja a kompatibilitást a szerverhez való csatlakozáskor. A hozzáadott tartalom elveszik, ha frissíted a szerverprofil tartalmát."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Letöltés"
|
||||
@@ -291,16 +291,16 @@
|
||||
"message": "Java előkészítése"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Java verzió {version} letöltése"
|
||||
"message": "A Java {version} letöltése..."
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Java verzió {version} kibontása"
|
||||
"message": "A Java {version} kibontása..."
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Java verzió ellőkészítése {version}"
|
||||
"message": "A Java {version} előkészítése..."
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Java verzió {version} előkészítése"
|
||||
"message": "A Java {version} előkészítése..."
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "A Java {version} ellenőrzése"
|
||||
@@ -324,7 +324,7 @@
|
||||
"message": "Betöltő folyamatok futtatása"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Az profilodhoz tartozó összes adat véglegesen törlődik, beleértve a világokat, a beállításokat és az összes telepített tartalmat."
|
||||
"message": "A profilodhoz tartozó összes adat véglegesen törlődik, beleértve a világokat, a beállításokat és az összes telepített tartalmat."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Ezt a műveletet nem lehet visszavonni"
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Keresés {count} világ között..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Ne töltse le"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Telepítés a játékhoz"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Telepítés mindenképpen"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Telepítés"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Ezt a meghívót nem a Modrinth, hanem egy másik Modrinth-felhasználó hozta létre. Csak olyan személyektől fogadd el a meghívásokat, akikben megbízol."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count} mod"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Egy fájlt csak akkor vizsgálnak meg, ha azt a Modrinth-en közzétették, függetlenül a fájlformátumtól (beleértve a .mrpack formátumot is)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Megosztott profil"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Ez a szerver-modpack olyan fájlokat tartalmaz, amelyek nem szerepelnek a Modrinth oldalon. Határozottan javasoljuk, hogy kizárólag olyan forrásokból telepíts fájlokat, amelyekben megbízol."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Figyelmeztetés ismeretlen fájlokról"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Felismerhetetlen fájlok"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Tartalom megtekintése"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Frissítsd a játékhoz"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Törölve"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Frissítés szükséges ehhez: {name}. Kérlek, frissíts a legújabb verzióra a játék elindításához."
|
||||
},
|
||||
@@ -456,7 +480,7 @@
|
||||
"message": "Vissza a böngészéshez"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Tartalom telepítése a példányba"
|
||||
"message": "Tartalom telepítése a profilba"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "Összes verzió"
|
||||
|
||||
@@ -422,24 +422,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Cerca tra {count} mondi..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Non installare"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installa per continuare"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Installa comunque"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installa"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Questo invito è stato creato da un utente e non da Modrinth. Accetta solo gli inviti da persone fidate."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count} mod"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Solo i file pubblicati su Modrinth vengono esaminati, qualunque sia il loro formato (.mrpack inclusi)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Istanza condivisa"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Questo pacchetto contiene dei file che non sono su Modrinth. Consigliamo di installare file solo da fonti attendibili."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "File sconosciuti"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "File non riconosciuti"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Mostra contenuti"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Aggiorna per continuare"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Rimosso"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "{name} richiede degli aggiornamenti. Installa l'ultima versione per poter giocare."
|
||||
},
|
||||
|
||||
@@ -143,6 +143,12 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "プレイ時間を表示"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Modパックの重複インストール、通常のコンテンツ削除、一括更新、Modパックのリンク解除、修復の促しなど、リスクの低い確認画面は自動的にスキップされます。危険な警告は引き続き表示されます。"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "重要でない警告をスキップ"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "サイドバーの表示/非表示を切り替える機能を有効にします。"
|
||||
},
|
||||
@@ -173,12 +179,18 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "すでに追加済み"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "インスタンスに戻る"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "コンテンツを探す"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "サーバーを探す"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "既に追加済みのサーバーを非表示にする"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modパック"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "실행 중인 인스턴스 숨기기"
|
||||
},
|
||||
"app.action-bar.install.copied-details": {
|
||||
"message": "복사됨"
|
||||
},
|
||||
"app.action-bar.install.copy-details": {
|
||||
"message": "세부 정보 복사"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "무시"
|
||||
},
|
||||
@@ -20,6 +26,66 @@
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "재시도"
|
||||
},
|
||||
"app.action-bar.install.summary.app-closing": {
|
||||
"message": "앱이 종료되어 취소되었습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "모드팩을 읽을 수 없음"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "취소됨"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "파일 정리가 완료되지 않았습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "파일을 다운로드할 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.corrupt-download": {
|
||||
"message": "다운로드한 파일이 손상되었습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.could-not-save-files": {
|
||||
"message": "파일을 저장할 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.download-failed": {
|
||||
"message": "다운로드를 완료할 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.instance-not-found": {
|
||||
"message": "인스턴스를 찾을 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-file-path": {
|
||||
"message": "파일 경로가 올바르지 않습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack": {
|
||||
"message": "모드팩 데이터가 올바르지 않습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack-files": {
|
||||
"message": "모드팩 파일이 올바르지 않은 메타데이터를 가지고 있습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.java-setup-failed": {
|
||||
"message": "Java 설치를 완료할 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.loader-setup-failed": {
|
||||
"message": "로더 설치에 실패했습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.local-data-error": {
|
||||
"message": "로컬 데이터를 업데이트할 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.minecraft-setup-failed": {
|
||||
"message": "Minecraft 설치에 실패했습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.modrinth-unreachable": {
|
||||
"message": "Modrinth에 연결할 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.no-write-permission": {
|
||||
"message": "쓰기 권한이 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.pack-download-failed": {
|
||||
"message": "팩을 다운로드할 수 없습니다"
|
||||
},
|
||||
"app.action-bar.install.summary.unexpected-error": {
|
||||
"message": "무언가 잘못되었습니다"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "알 수 없는 인스턴스"
|
||||
},
|
||||
@@ -218,6 +284,9 @@
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "마무리 중"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "설치 대기열에 추가됨"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Java 준비 중"
|
||||
},
|
||||
@@ -356,36 +425,75 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "{count}개의 세계 검색..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "설치하지 않음"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "설치하고 플레이"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "무시하고 설치"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "설치"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "이 초대장은 다른 Modrinth 사용자가 생성했으며, Modrinth에서 생성하지 않았습니다. 신뢰할 수 있는 초대장만 수락하십시오."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {모드 #개} other {모드 #개}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "모든 파일은 형식(.mrpack 포함)에 무관하게 Modrinth에 게시되어야만 검수를 거칩니다."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "인스턴스 공유됨"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "이 서버의 모드팩은 Modrinth에 게시되지 않은 파일을 포함하고 있습니다. 신뢰할 수 있는 출처의 파일만 설치하는 것을 강력히 권장합니다."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "출처를 알 수 없는 파일"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "확인되지 않은 파일"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "구성 요소 보기"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "업데이트하고 플레이"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "제거됨"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "{name}을(를) 플레이하려면 업데이트가 필요합니다. 게임을 실행하려면 최신 버전으로 업데이트해 주세요."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "이미 설치되어 있음"
|
||||
},
|
||||
"app.project.install-button.switch-version": {
|
||||
"message": "버전 변경"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "탐색으로 돌아가기"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "인스턴스에 콘텐츠 설치"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "모든 버전"
|
||||
},
|
||||
"app.project.version.download-in-browser": {
|
||||
"message": "브라우저에서 다운로드"
|
||||
},
|
||||
"app.project.version.installing": {
|
||||
"message": "설치 중"
|
||||
},
|
||||
"app.project.versions.already-installed": {
|
||||
"message": "이미 설치됨"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "개발자 모드가 활성화되었습니다."
|
||||
},
|
||||
@@ -579,7 +687,7 @@
|
||||
"message": "다운로드 완료"
|
||||
},
|
||||
"app.update-popup.reload": {
|
||||
"message": "업데이트하려면 다시 로드"
|
||||
"message": "새로고침하여 업데이트"
|
||||
},
|
||||
"app.update-popup.title": {
|
||||
"message": "업데이트 가능"
|
||||
|
||||
@@ -11,6 +11,87 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Verberg meer actieve instanties"
|
||||
},
|
||||
"app.action-bar.install.copied-details": {
|
||||
"message": "Gekopieerd"
|
||||
},
|
||||
"app.action-bar.install.copy-details": {
|
||||
"message": "Details kopiëren"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Sluiten"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Instantie openen"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Opnieuw proberen"
|
||||
},
|
||||
"app.action-bar.install.summary.app-closing": {
|
||||
"message": "Geannuleerd vanwege het sluiten van de app"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "Modpack kon niet gelezen worden"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "Geannuleerd"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "Opschonen is niet voltooid"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "Bestanden konden niet gedownload worden"
|
||||
},
|
||||
"app.action-bar.install.summary.corrupt-download": {
|
||||
"message": "Gedownloade bestand is beschadigd"
|
||||
},
|
||||
"app.action-bar.install.summary.could-not-save-files": {
|
||||
"message": "Bestanden konden niet opgeslagen worden"
|
||||
},
|
||||
"app.action-bar.install.summary.download-failed": {
|
||||
"message": "Downloaden kon niet worden voltooid"
|
||||
},
|
||||
"app.action-bar.install.summary.instance-not-found": {
|
||||
"message": "Instantie kon niet gevonden worden"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-file-path": {
|
||||
"message": "Bestandspad is ongeldig"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack": {
|
||||
"message": "Modpack-gegevens ongeldig"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack-files": {
|
||||
"message": "Modpack-bestanden bevatten ongeldige metagegevens"
|
||||
},
|
||||
"app.action-bar.install.summary.java-setup-failed": {
|
||||
"message": "Installatie van Java kon niet worden voltooid"
|
||||
},
|
||||
"app.action-bar.install.summary.loader-setup-failed": {
|
||||
"message": "Instellen van loader is mislukt"
|
||||
},
|
||||
"app.action-bar.install.summary.local-data-error": {
|
||||
"message": "Lokale gegevens konden niet bijgewerkt worden"
|
||||
},
|
||||
"app.action-bar.install.summary.minecraft-setup-failed": {
|
||||
"message": "Instellen van Minecraft is mislukt"
|
||||
},
|
||||
"app.action-bar.install.summary.modrinth-unreachable": {
|
||||
"message": "Modrinth kon niet bereikt worden"
|
||||
},
|
||||
"app.action-bar.install.summary.no-write-permission": {
|
||||
"message": "Geen schrijfrechten"
|
||||
},
|
||||
"app.action-bar.install.summary.pack-download-failed": {
|
||||
"message": "Pack kon niet gedownload worden"
|
||||
},
|
||||
"app.action-bar.install.summary.unexpected-error": {
|
||||
"message": "Er is iets misgegaan"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Onbekende instantie"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Installaties"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Maak primaire instantie"
|
||||
},
|
||||
@@ -101,6 +182,12 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Speeltijd laten zien"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Bevestigingen met een laag risico, zoals het installeren van dubbele modpacks, het verwijderen van normale inhoud, bulkupdates, het ontkoppelen van modpacks en reparatieverzoeken, worden automatisch overgeslagen. Waarschuwingen voor gevaarlijke dingen worden nog steeds weergegeven."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Niet-essentiële waarschuwingen overslaan"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Schakelt de mogelijkheid om de zijbalk in- of uit te schakelen in."
|
||||
},
|
||||
@@ -141,16 +228,19 @@
|
||||
"message": "Terug naar instantie"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Ontdek content"
|
||||
"message": "Inhoud ontdekken"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Ontdek servers"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Al toegevoegde servers verbergen"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Het toevoegen van content kan de compatibiliteit verbreken bij het verbinden met de server. Alle toegevoegde inhoud gaat ook verloren wanneer u de content van de serverinstantie bijwerkt."
|
||||
"message": "Het toevoegen van inhoud kan de compatibiliteit verstoren bij het verbinden met de server. Eventuele toegevoegde inhoud gaat bovendien verloren wanneer je de inhoud van de serverinstantie bijwerkt."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installeren"
|
||||
@@ -167,6 +257,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exporteer modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Naam van modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack naam"
|
||||
},
|
||||
@@ -176,8 +269,62 @@
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Inhoud aan het downloaden"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Minecraft aan het downloaden"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Pakketbestand aan het downloaden"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Overschrijvingen eruit halen"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Aan het afronden"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "In de wachtrij voor installatie"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Java aan het voorbereiden"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Java {version} aan het downloaden"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Java {version} aan het uitpakken"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Java {version} aan het ophalen"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Java {version} aan het voorbereiden"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Java {version} aan het valideren"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Paketmanifest aan het lezen"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Loader wordt bepaald"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Minecraft wordt bepaald"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Inhoud aan het bepalen"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Aan het terugdraaien"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Loader-processors aan het uitvoeren"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Alle data van je instantie zal permanent verwijderd worden, inclusief al je werelden, voorkeuren, en alle geïnstalleerde content."
|
||||
"message": "Alle gegevens van je instantie worden definitief verwijderd, inclusief je werelden, configuraties en alle geïnstalleerde inhoud."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Deze actie kan niet ongedaan gemaakt worden"
|
||||
@@ -200,6 +347,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instantie"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "{current, number}/{total, number} projecten aan het downloaden..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Update aan het afronden..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Versies worden bepaald..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "project"
|
||||
},
|
||||
@@ -224,6 +380,18 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Zoek servers"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Wereld verwijderen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Wereld verwijderen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Deze wereld wordt definitief uit deze instantie verwijderd. Deze actie kan niet ongedaan worden gemaakt."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "{name} verwijderen"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Gemod"
|
||||
},
|
||||
@@ -242,38 +410,89 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Noch servers noch werelden toegevoegd"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Server verwijderen"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Server verwijderen"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Deze server wordt verwijderd uit je serverlijst en uit de serverlijst in het spel. Je kunt hem later weer toevoegen als je het adres weet."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "{name} verwijderen"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Zoek werelden"
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Niet installeren"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installeer om te spelen"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Toch installeren"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installeer"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Deze uitnodiging is aangemaakt door een andere Modrinth-gebruiker, niet door Modrinth zelf. Accepteer alleen uitnodigingen van mensen die je vertrouwt."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural,one {# mod}other {# mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Een bestand wordt alleen beoordeeld als het op Modrinth wordt gepubliceerd, ongeacht het bestandsformaat (inclusief .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Gedeelde instantie"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Dit server-modpack bevat bestanden die niet op Modrinth zijn gepubliceerd. We raden je ten zeerste aan om alleen bestanden te installeren uit bronnen die je vertrouwt."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Waarschuwing voor onbekende bestanden"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Onbekende bestanden"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Toon content"
|
||||
"message": "Inhoud bekijken"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Update om te spelen"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Verwijderde"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Een update is vereist om {name} te spelen. Update naar de laatste versie om het spel te starten."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Dit project is al geïnstalleerd"
|
||||
},
|
||||
"app.project.install-button.switch-version": {
|
||||
"message": "Versie wijzigen"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Terug naar Ontdekken"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Installeer content naar instantie"
|
||||
"message": "Inhoud op instantie installeren"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "Alle versies"
|
||||
},
|
||||
"app.project.version.download-in-browser": {
|
||||
"message": "Downloaden in de browser"
|
||||
},
|
||||
"app.project.version.installing": {
|
||||
"message": "Wordt geïnstalleerd"
|
||||
},
|
||||
"app.project.versions.already-installed": {
|
||||
"message": "Al geïnstalleerd"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Ontwikkelaarsmodus ingeschakeld."
|
||||
@@ -390,11 +609,14 @@
|
||||
"message": "Je skinvolgorde kon niet worden opgeslagen."
|
||||
},
|
||||
"app.skins.reorder-error.title": {
|
||||
"message": "Het herschikken van skins is mislukt."
|
||||
"message": "Herschikken van skins is mislukt"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chaos-cubed": {
|
||||
"message": "Chaos Cubed"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
@@ -621,7 +843,7 @@
|
||||
"message": "Verwijder instantie"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Verwijdert permanent een instantie van je apparaat, inclusief je werelden, instellingen en alle geïnstalleerde inhoud. Wees voorzichtig, zodra je een instantie verwijdert, is het niet meer mogelijk om deze te herstellen."
|
||||
"message": "Hiermee wordt een instantie definitief van je apparaat verwijderd, inclusief je werelden, instellingen en alle geïnstalleerde inhoud. Wees voorzichtig, want zodra je een instantie hebt verwijderd, kun je deze niet meer herstellen."
|
||||
},
|
||||
"instance.settings.tabs.general.deleting.button": {
|
||||
"message": "Verwijderen..."
|
||||
@@ -812,6 +1034,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Kopieer adres"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Snelkoppeling maken"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Niet tonen op de startpagina"
|
||||
},
|
||||
|
||||
@@ -422,24 +422,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Szukaj wśród {count} światów..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Nie instaluj"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Zainstaluj, aby grać"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Instaluj mimo to"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Zainstaluj"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "To zaproszenie zostało utworzone przez innego użytkownika Modrinth, nie przez firmę Modrinth. Tylko akceptuj zaproszenia od osób, którym ufasz."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# mod} few {# mody} other {# modów}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Plik jest sprawdzony tylko, jeżeli został opublikowany na Modrinth, niezależnie od jego formatu (w tym pliki .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Wspólna instancja"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Ta serwerowa paczka modów zawiera pliki, które nie zostały opublikowane na Modrinth. Zalecamy instalowanie plików tylko ze źródeł, którym ufasz."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Ostrzeżenie o nieznanych plikach"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Nieznane plki"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Pokaż zawartość"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Zaktualizuj, by grać"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Usunięto"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Aktualizacja jest wymagana, aby grać w {name}. Proszę zaktualizować do najnowszej wersji, aby uruchomić grę."
|
||||
},
|
||||
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Buscar {count} mundos..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Não instalar"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Instale para jogar"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Instalar mesmo assim"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Instalar"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Este convite foi criado por outro usuário do Modrinth, não pelo próprio Modrinth. Só aceite convites de pessoas que você confia."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, =0 {Nenhum mod} one {# mod} other {# mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Um arquivo só é revisado se for publicado no Modrinth, independente do formato de arquivo (incluindo .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Instância compartilhada"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Este pacote de mods do servidor contém arquivos não publicados no Modrinth. Recomendamos fortemente a instalar apenas arquivos de fontes que você confia."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Aviso de arquivos desconhecidos"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Arquivos não reconhecidos"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Ver conteúdo"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Atualize para jogar"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Removido"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Uma atualização é necessária para jogar {name}. Atualize para a versão mais recente para iniciar o jogo."
|
||||
},
|
||||
|
||||
@@ -419,24 +419,48 @@
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Удаление {name}"
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Не устанавливать"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Установка перед запуском"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Всё равно установить"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Установить"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Это приглашение создано другим пользователем Modrinth. Принимайте приглашения только от тех, кому доверяете."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# мод} few {# мода} other {# модов}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Файл проверяется только если он опубликован на Modrinth, независимо от его формата (включая .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Сборка"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Сборка этого сервера содержит файлы, которые не опубликованы на Modrinth. Рекомендуется скачивать файлы только из надёжных источников."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Предупреждение о неизвестных файлах"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Нераспознанные файлы"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Посмотреть содержимое"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Обновление перед запуском"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Удалён"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Обновите {name} до последней версии, чтобы запустить игру."
|
||||
},
|
||||
|
||||
@@ -29,12 +29,63 @@
|
||||
"app.action-bar.install.summary.app-closing": {
|
||||
"message": "Otkazano zbog zatvaranja aplikacije"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "Nije moglo pročitati modpack"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "Otkazano"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "Čišćenje nije završeno"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "Nije bilo moguće instalirati datoteke"
|
||||
},
|
||||
"app.action-bar.install.summary.corrupt-download": {
|
||||
"message": "Preuzeta datoteka je oštećena"
|
||||
},
|
||||
"app.action-bar.install.summary.could-not-save-files": {
|
||||
"message": "Nije bilo moguće spremiti datoteke"
|
||||
},
|
||||
"app.action-bar.install.summary.download-failed": {
|
||||
"message": "Preuzimanje nije završeno"
|
||||
},
|
||||
"app.action-bar.install.summary.instance-not-found": {
|
||||
"message": "Instanca nije pronađena"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-file-path": {
|
||||
"message": "Put do datoteke nije ispravan"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack": {
|
||||
"message": "Podaci modpaka su nevažeći"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack-files": {
|
||||
"message": "Datoteke modpaka sadrže nevažeće metapodatke"
|
||||
},
|
||||
"app.action-bar.install.summary.java-setup-failed": {
|
||||
"message": "Podešavanje Jave nije moglo biti završeno"
|
||||
},
|
||||
"app.action-bar.install.summary.loader-setup-failed": {
|
||||
"message": "Podešavanje učitavača nije uspelo"
|
||||
},
|
||||
"app.action-bar.install.summary.local-data-error": {
|
||||
"message": "Nije bilo moguće ažurirati lokalne podatke"
|
||||
},
|
||||
"app.action-bar.install.summary.minecraft-setup-failed": {
|
||||
"message": "Podešavanje Minecrafta nije uspelo"
|
||||
},
|
||||
"app.action-bar.install.summary.modrinth-unreachable": {
|
||||
"message": "Nije bilo moguće povezati se sa Modrinth-om"
|
||||
},
|
||||
"app.action-bar.install.summary.no-write-permission": {
|
||||
"message": "Nema dozvole za pisanje"
|
||||
},
|
||||
"app.action-bar.install.summary.pack-download-failed": {
|
||||
"message": "Nije bilo moguće instalirati datoteku"
|
||||
},
|
||||
"app.action-bar.install.summary.unexpected-error": {
|
||||
"message": "Nešto je pošlo po zlu"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Nepoznata instanca"
|
||||
},
|
||||
@@ -233,6 +284,9 @@
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Finaliziranje"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Na čekanju za instalaciju"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Pripremanje Jave"
|
||||
},
|
||||
@@ -371,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Pretraži {count} svetova..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Ne instaliraj"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Instaliraj da bi igrao"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Svejedno instaliraj"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Instaliraj"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Ovu pozivnicu je kreirao drugi korisnik Modrintha, a ne sam Modrinth. Prihvataj pozivnice samo od osoba kojima veruješ."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# mod} other {# modova}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Datoteka se pregleda samo ako je postavljena na Modrinth, bez obzira na njen format (uključujući .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Deljena instanca"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Ovaj modpack servera sadrži datoteke koje nisu postavljene na Modrinth-u. Preporučujemo da instaliraš datoteke samo iz izvora kojima veruješ."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Upozorenje nepoznatih datoteka"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Nepoznate datoteke"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Pogledaj sadržaj"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Ažuriraj da bi igrao"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Smaknuto"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Potrebno je ažuriranje da biste igrali {name}. Molimo te da ažuriraš na najnoviju verziju da bi pokrenuo igru."
|
||||
},
|
||||
|
||||
@@ -407,9 +407,15 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Sök {count} världar..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Installera inte"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installera för att spela"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Installera andå"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installera"
|
||||
},
|
||||
@@ -425,6 +431,9 @@
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Uppdatera för att spela"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Tog bort"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "En uppdatering krävs för att spela {name}. Vänligen uppdatera till senaste version för att starta spelet."
|
||||
},
|
||||
@@ -615,7 +624,7 @@
|
||||
"message": "Logga in"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Snälla logga in din Minecraft konto att använda hud hantering förmågor i Modrinth appen."
|
||||
"message": "Vänligen logga in på ditt Minecraft-konto för att använda utseendehanteringfunktioner i Modrinth appen."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Exalterad Modrinth bot"
|
||||
@@ -657,7 +666,7 @@
|
||||
"message": "Version {version} har installerats!"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
"message": "exempel.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Välj ett alternativ"
|
||||
@@ -732,7 +741,7 @@
|
||||
"message": "Sök efter vänner..."
|
||||
},
|
||||
"friends.section.heading": {
|
||||
"message": "{title}: {count}"
|
||||
"message": "{title} - {count}"
|
||||
},
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Logga in på ett Modrinth-konto</link> för att lägga till vänner och se vad de spelar!"
|
||||
@@ -852,19 +861,19 @@
|
||||
"message": "Alfa"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.alpha.description": {
|
||||
"message": "Utgåvor, beta och alfa versioner visas som tillgängliga uppdateringar."
|
||||
"message": "Release-, beta- och alfaversioner kommer visas som tillgängliga uppdateringar."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.beta.description": {
|
||||
"message": "Utgåvor och beta versioner visas som tillgängliga uppdateringar."
|
||||
"message": "Release- och betaversioner kommer visas som tillgängliga uppdateringar."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.release": {
|
||||
"message": "Utgåva"
|
||||
"message": "Release"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.release.description": {
|
||||
"message": "Endast utgivna versioner visas som tillgängliga uppdateringar."
|
||||
"message": "Endast realeseversioner kommer visas som tillgängliga uppdateringar."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.select": {
|
||||
"message": "Välj uppdateringskanal"
|
||||
@@ -930,10 +939,10 @@
|
||||
"message": "Anpassad minnestilldelning"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Skriv in miljövariabler..."
|
||||
"message": "Ange miljövariabler..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Skriv in Java-argument..."
|
||||
"message": "Ange Java-argument..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Miljövariabler"
|
||||
@@ -996,13 +1005,13 @@
|
||||
"message": "Skapa genväg"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Visa inte på Startsidan"
|
||||
"message": "Visa inte på startsidan"
|
||||
},
|
||||
"instance.worlds.game_already_open": {
|
||||
"message": "Instans är redan öppen"
|
||||
},
|
||||
"instance.worlds.hardcore": {
|
||||
"message": "Hardcore läge"
|
||||
"message": "Hardcore-läge"
|
||||
},
|
||||
"instance.worlds.incompatible_server": {
|
||||
"message": "Servern är inkompatibel"
|
||||
@@ -1014,10 +1023,10 @@
|
||||
"message": "Servern kunde inte kontaktas"
|
||||
},
|
||||
"instance.worlds.no_server_quick_play": {
|
||||
"message": "Du kan bara hoppa rakt in till servers på Minecraft Alfa 1.0.5+"
|
||||
"message": "Du kan endast gå direkt in i servrar på Minecraft Alfa 1.0.5+"
|
||||
},
|
||||
"instance.worlds.no_singleplayer_quick_play": {
|
||||
"message": "Du kan bara hoppa rakt in till enspelarläge världar på Minecraft 1,20+"
|
||||
"message": "Du kan endast gå direkt in i enspelarläge världar på Minecraft 1.20+"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "Spela instans"
|
||||
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "{count} dünya ara..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "İndirme"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Oynamak için yükleyin"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Yine de indir"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Yükle"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Bu davetiye Modrinth tarafından değil, başka bir Modrinth kullanıcısı tarafından oluşturulmuştur. Sadece güvendiğiniz kişilerden gelen davetleri kabul edin."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {#mod} other {#mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Dosya formatı ne olursa olsun (.mrpack dahil), bir dosya yalnızca Modrinth'e yüklendiğinde denetlenir."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Paylaşılan Kurulum"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Bu sunucunun mod paketi Modrinth'te yayınlanmamış doysaları mevcut. Yalnızca güvendiğiniz kaynaklardan dosya indirmenizi önemle tavsiye ederiz."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Bilinmeyen dosya uyarısı"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Tanımlanamayan dosyalar"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "İçeriği görüntüle"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Oynamak için güncelle"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Silindi"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "{name} oyununu oynamak için güncelleme gereklidir. Oyunu başlatmak için lütfen en son sürüme güncelleyin."
|
||||
},
|
||||
|
||||
@@ -30,11 +30,14 @@
|
||||
"message": "Скасовано через закриття додатку"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "Не вдалось прочитати збірку модів"
|
||||
"message": "Не вдалось прочитати збірку модифікацій"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "Скасовано"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "Очищення не завершено"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "Не вдалося завантажити файли"
|
||||
},
|
||||
@@ -57,7 +60,7 @@
|
||||
"message": "Дані збірки модів недійсні"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack-files": {
|
||||
"message": "Файли збірки модів містять не дійсні метадані"
|
||||
"message": "Файли збірки модифікацій містять не дійсні метадані"
|
||||
},
|
||||
"app.action-bar.install.summary.java-setup-failed": {
|
||||
"message": "Не вдалося завершити встановлення Java"
|
||||
@@ -86,6 +89,9 @@
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Невідомий екземпляр"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Установки"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Зробити основним профілем"
|
||||
},
|
||||
@@ -177,7 +183,7 @@
|
||||
"message": "Час гри"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Автоматично пропускає підтвердження з низьким рівнем ризику, як-от повторне встановлення збірки модів, видалення звичайного вмісту, масові оновлення, відвʼязування збірки модів та записи на відновлення. Небезпечні попередження все одно зображуватимуться."
|
||||
"message": "Автоматично пропускає підтвердження з низьким рівнем ризику, такі як встановлення дублікатів інсталяцій, звичайне видалення вмісту, масові оновлення, від’єднання інсталяцій та запити на відновлення. Попередження про небезпеку й надалі будуть показуватися."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Пропустити несуттєві попередження"
|
||||
@@ -252,7 +258,7 @@
|
||||
"message": "Експортувати збірку"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Назва збірки модів"
|
||||
"message": "Назва збірки"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Назва збірки"
|
||||
@@ -344,6 +350,12 @@
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "Завантаження проєктів: {current, number}/{total, number}..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Завершуємо оновлення…"
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Знаходимо версії…"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "проєкт"
|
||||
},
|
||||
@@ -368,6 +380,18 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Переглянути сервери"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Видалити світ"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Видалити світ"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Цей світ буде видалено назавжди з цього екземпляра. Це не можна скасувати."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Видалення {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Модифіковано"
|
||||
},
|
||||
@@ -386,39 +410,90 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Сервера або світи не додано"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Видалити сервер"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Видалити сервер"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Цей сервер буде видалено з вашого списку серверів. Ви зможете повернути його, якщо знаєте адресу."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Видалення {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Пошук {count} світів…"
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Не встановлювати"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Установлення для гри"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Усе одно встановити"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Установити"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "Це запрошення створив інший користувач Modrinth, не сам Modrinth. Приймайте запрошення лише від тих людей, яким довіряєте."
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# мод} few {# мода} other {# модів}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "Файл перевірятиметься, лише якщо його завантажено на Modrinth, незалежно від його формату (включно з .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Профіль"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "Ця збірка сервера містить файли, які не є опубліковані на Modrinth. Ми наполегливо рекомендуємо встановлювати лише ті файли, яким довіряєте."
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "Попередження про невідомі файли"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "Нерозпізнані файли"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Дивитися вміст"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Оновлення для гри"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Видалено"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "«{name}» потребує оновлення, щоб грати. Будь ласка, оновіть до останньої версії, щоб запустити гру."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Проєкт уже встановлено"
|
||||
},
|
||||
"app.project.install-button.switch-version": {
|
||||
"message": "Змінити версію"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Повернутися до дослідження"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Установити вміст до профілю"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "Усі версії"
|
||||
},
|
||||
"app.project.version.download-in-browser": {
|
||||
"message": "Завантажити в браузері"
|
||||
},
|
||||
"app.project.version.installing": {
|
||||
"message": "Установлення"
|
||||
},
|
||||
"app.project.versions.already-installed": {
|
||||
"message": "Уже встановлено"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Увімкнено режим розробника."
|
||||
},
|
||||
@@ -539,6 +614,9 @@
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chaos-cubed": {
|
||||
"message": "Chaos Cubed"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
@@ -956,6 +1034,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Скопіювати адресу"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Створити ярлик"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Не показувати на головній"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,33 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Đóng các phiên bản đang chạy"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Thử lại"
|
||||
},
|
||||
"app.action-bar.install.summary.app-closing": {
|
||||
"message": "Đã hủy do đóng ứng dụng"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "Không đọc được modpack"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "Đã hủy"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "Dọn dẹp không hoàn thện"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "Không tải được tệp"
|
||||
},
|
||||
"app.action-bar.install.summary.corrupt-download": {
|
||||
"message": "Tệp được tải bị hỏng"
|
||||
},
|
||||
"app.action-bar.install.summary.could-not-save-files": {
|
||||
"message": "Không lưu được tệp"
|
||||
},
|
||||
"app.action-bar.install.summary.download-failed": {
|
||||
"message": "Không hoàn tất tải xuống"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Đặt làm bản instance chính"
|
||||
},
|
||||
@@ -224,6 +251,15 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Tìm kiếm máy chủ"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Xóa thế giới"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Xóa thế giới"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Đang xóa {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modded"
|
||||
},
|
||||
@@ -242,12 +278,27 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Không có máy chủ hay thế giới nào đã được thêm"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Bỏ khỏi danh sách"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Xóa máy chủ"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "{name} sẽ bị xóa"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Tìm kiếm {count} world..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "Ngừng tải"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Tải xuống để chơi"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Tải tiếp"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Tải xuống"
|
||||
},
|
||||
@@ -263,18 +314,36 @@
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Cập nhật và bắt đầu chơi"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "Đã bỏ"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Bạn cần cập nhật {name} để có thể chơi. Vui lòng cập nhật lên bản mới nhất để khởi chạy trò chơi."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Dự án này đã được cài đặt"
|
||||
},
|
||||
"app.project.install-button.switch-version": {
|
||||
"message": "Đổi phiên bản"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Quay lại trang Khám Phá"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Cài đặt nội dung vào phiên bản"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "Tất cả phiên bản"
|
||||
},
|
||||
"app.project.version.download-in-browser": {
|
||||
"message": "Tải trong trình duyệt"
|
||||
},
|
||||
"app.project.version.installing": {
|
||||
"message": "Đang tải"
|
||||
},
|
||||
"app.project.versions.already-installed": {
|
||||
"message": "Đã có sẵn"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Chế độ nhà phát triển đã được bật."
|
||||
},
|
||||
@@ -395,6 +464,9 @@
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chaos-cubed": {
|
||||
"message": "Chaos Cubed"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
@@ -812,6 +884,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Sao chép địa chỉ"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Tạo lối tắt"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Không hiện ở Trang chủ"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"message": "已复制"
|
||||
},
|
||||
"app.action-bar.install.copy-details": {
|
||||
"message": "复制详情"
|
||||
"message": "复制详细信息"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "忽略"
|
||||
@@ -30,13 +30,13 @@
|
||||
"message": "由于应用关闭而取消"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "无法读取整合包"
|
||||
"message": "整合包读取失败"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "已取消"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "清理未完成"
|
||||
"message": "未完成清理"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "无法下载文件"
|
||||
@@ -48,7 +48,7 @@
|
||||
"message": "无法保存文件"
|
||||
},
|
||||
"app.action-bar.install.summary.download-failed": {
|
||||
"message": "下载无法完成"
|
||||
"message": "无法完成下载"
|
||||
},
|
||||
"app.action-bar.install.summary.instance-not-found": {
|
||||
"message": "无法找到实例"
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "搜索 {count} 个世界……"
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "不要安装"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "安装以游玩"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "无视风险继续安装"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "安装"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "这个邀请是由另一位 Modrinth 用户创建的,而不是 Modrinth 官方。只接受你信任的人的邀请。"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count} 个模组"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "只有上传到 Modrinth 的文件才会经过审核,无论其文件格式如何(包括 .mrpack)。"
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "共享实例"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "这个整合包包含一些未在 Modrinth 发布的文件。我们强烈建议只从你信任的来源安装文件。"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "未知文件警告"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "未识别文件"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "查看内容"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "更新以游玩"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "已移除"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "需要更新至最新版本才能运行 {name}。请更新后启动游戏。"
|
||||
},
|
||||
|
||||
@@ -425,24 +425,48 @@
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "搜尋 {count} 個世界..."
|
||||
},
|
||||
"app.modal.install-to-play.dont-install": {
|
||||
"message": "不要安裝"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "安裝以遊玩"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "仍要安裝"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "安裝"
|
||||
},
|
||||
"app.modal.install-to-play.invite-warning": {
|
||||
"message": "這份邀請是由另一位 Modrinth 使用者建立,並非由 Modrinth 官方建立。請僅接受來自你信任的人的邀請。"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, other {# 個模組}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "只有在 Modrinth 上發布的檔案才會經過審查,無論其檔案格式為何(包含 .mrpack)。"
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "共用實例"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-description": {
|
||||
"message": "這個伺服器模組包中包含未在 Modrinth 上發布的檔案。強烈建議你僅安裝來自信任來源的檔案。"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "未知檔案警告"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "無法辨識的檔案"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "檢視內容"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "更新以遊玩"
|
||||
},
|
||||
"app.modal.update-to-play.removed": {
|
||||
"message": "已移除"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "需要更新才能遊玩「{name}」。請更新至最新版本以啟動遊戲。"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
SkinPreviewRenderer,
|
||||
Toggle,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
@@ -26,13 +27,18 @@ import { computedAsync } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import EarsModIcon from '@/assets/skins/ears-mod.png'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
|
||||
import VirtualSkinSectionList from '@/components/ui/skin/VirtualSkinSectionList.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable, get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import {
|
||||
generateSkinPreviews,
|
||||
getSkinPreviewKey,
|
||||
skinBlobUrlMap,
|
||||
} from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import type { Cape, Skin, SkinTextureUrl } from '@/helpers/skins.ts'
|
||||
import {
|
||||
equip_skin,
|
||||
@@ -60,6 +66,7 @@ type VirtualSkinSectionListExpose = {
|
||||
|
||||
const PENDING_SKIN_REFRESH_DELAY_MS = 11_000
|
||||
const DEFAULT_SKIN_SECTION_SORT_ORDER = ['Default skins', 'Modrinth Pride']
|
||||
const EARS_NOTICE_PLACEHOLDER = '__EARS_MOD_NAME__'
|
||||
const messages = defineMessages({
|
||||
skinSelectorTitle: {
|
||||
id: 'app.skins.title',
|
||||
@@ -163,6 +170,18 @@ const messages = defineMessages({
|
||||
id: 'app.skins.preview.edit-button',
|
||||
defaultMessage: 'Edit skin',
|
||||
},
|
||||
earsFeatureNotice: {
|
||||
id: 'app.skins.ears-feature-notice',
|
||||
defaultMessage: 'This skin uses features from the {ears} mod',
|
||||
},
|
||||
toggleEarsFeaturesOff: {
|
||||
id: 'app.skins.toggle-ears-features-off',
|
||||
defaultMessage: 'Toggle off',
|
||||
},
|
||||
toggleEarsFeaturesOn: {
|
||||
id: 'app.skins.toggle-ears-features-on',
|
||||
defaultMessage: 'Toggle on',
|
||||
},
|
||||
excitedRinthbotAlt: {
|
||||
id: 'app.skins.sign-in.rinthbot-alt',
|
||||
defaultMessage: 'Excited Modrinth Bot',
|
||||
@@ -204,8 +223,30 @@ const currentUserId = ref<string | undefined>(undefined)
|
||||
const username = computed(() => currentUser.value?.profile?.name ?? undefined)
|
||||
const selectedSkin = ref<Skin | null>(null)
|
||||
const isApplyingSkin = ref(false)
|
||||
const earsFeaturesEnabled = ref(true)
|
||||
const selectedSkinHasEarsFeatures = ref(false)
|
||||
|
||||
const originalSelectedSkin = ref<Skin | null>(null)
|
||||
const earsFeatureNoticeParts = computed(() => {
|
||||
const notice = formatMessage(messages.earsFeatureNotice, {
|
||||
ears: EARS_NOTICE_PLACEHOLDER,
|
||||
})
|
||||
const placeholderIndex = notice.indexOf(EARS_NOTICE_PLACEHOLDER)
|
||||
|
||||
if (placeholderIndex === -1) {
|
||||
return {
|
||||
before: notice,
|
||||
after: '',
|
||||
hasEarsLink: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
before: notice.slice(0, placeholderIndex),
|
||||
after: notice.slice(placeholderIndex + EARS_NOTICE_PLACEHOLDER.length),
|
||||
hasEarsLink: true,
|
||||
}
|
||||
})
|
||||
|
||||
const savedSkins = computed(() => {
|
||||
try {
|
||||
@@ -755,8 +796,7 @@ async function loadCurrentUser() {
|
||||
}
|
||||
|
||||
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
return skinBlobUrlMap.get(key)
|
||||
return skinBlobUrlMap.get(getSkinPreviewKey(skin))
|
||||
}
|
||||
|
||||
async function login() {
|
||||
@@ -940,6 +980,10 @@ watch(
|
||||
() => {},
|
||||
)
|
||||
|
||||
watch(selectedSkin, () => {
|
||||
earsFeaturesEnabled.value = true
|
||||
})
|
||||
|
||||
watch(isSkinManagementReadOnly, (readOnly) => {
|
||||
if (readOnly) {
|
||||
isDraggingSkinFile.value = false
|
||||
@@ -1035,9 +1079,12 @@ await loadSkins()
|
||||
<SkinPreviewRenderer
|
||||
:cape-src="capeTexture"
|
||||
:texture-src="skinTexture || ''"
|
||||
:ears-texture-src="selectedSkin?.texture"
|
||||
:variant="skinVariant"
|
||||
:nametag="skinNametag"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
:ears-enabled="earsFeaturesEnabled"
|
||||
@ears-features-detected="selectedSkinHasEarsFeatures = $event"
|
||||
>
|
||||
<template v-if="hasPendingSkinChange" #nametag-badge>
|
||||
<div
|
||||
@@ -1049,36 +1096,126 @@ await loadSkins()
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<div
|
||||
v-if="hasPendingSkinChange"
|
||||
class="flex max-w-[calc(100vw-2rem)] flex-wrap items-center justify-center gap-2 px-2"
|
||||
class="skin-preview-subtitle flex w-full flex-col items-center gap-6"
|
||||
:class="{ 'has-ears-features': selectedSkinHasEarsFeatures }"
|
||||
>
|
||||
<button
|
||||
class="flex h-10 min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] border-0 bg-surface-4 px-4 py-2.5 text-base font-semibold leading-5 text-contrast shadow-md transition-[filter,transform] duration-200 enabled:hover:brightness-[--hover-brightness] enabled:focus-visible:brightness-[--hover-brightness] enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:size-5 [&>svg]:shrink-0"
|
||||
:disabled="isApplyingSkin || isSkinManagementReadOnly"
|
||||
@click="resetSelectedSkin"
|
||||
<div
|
||||
v-if="hasPendingSkinChange"
|
||||
class="skin-preview-actions flex w-full items-center justify-center gap-1.5"
|
||||
:class="selectedSkinHasEarsFeatures ? 'flex-nowrap' : 'flex-wrap'"
|
||||
>
|
||||
<RotateCounterClockwiseIcon />
|
||||
{{ formatMessage(commonMessages.resetButton) }}
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="
|
||||
selectedSkinHasEarsFeatures
|
||||
? formatMessage(commonMessages.resetButton)
|
||||
: undefined
|
||||
"
|
||||
class="skin-preview-action-button flex h-10 min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] border-0 bg-surface-4 px-4 py-2.5 text-base font-semibold leading-5 text-contrast shadow-md transition-[filter,transform] duration-200 enabled:hover:brightness-[--hover-brightness] enabled:focus-visible:brightness-[--hover-brightness] enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:size-5 [&>svg]:shrink-0"
|
||||
:disabled="isApplyingSkin || isSkinManagementReadOnly"
|
||||
:aria-label="formatMessage(commonMessages.resetButton)"
|
||||
@click="resetSelectedSkin"
|
||||
>
|
||||
<RotateCounterClockwiseIcon />
|
||||
<span class="skin-preview-action-label">
|
||||
{{ formatMessage(commonMessages.resetButton) }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="
|
||||
selectedSkinHasEarsFeatures ? formatMessage(messages.applyButton) : undefined
|
||||
"
|
||||
class="skin-preview-action-button flex h-10 min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] border-0 bg-brand px-4 py-2.5 text-base font-semibold leading-5 text-[rgba(0,0,0,0.9)] shadow-md transition-[filter,transform] duration-200 enabled:hover:brightness-[--hover-brightness] enabled:focus-visible:brightness-[--hover-brightness] enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:size-5 [&>svg]:shrink-0"
|
||||
:disabled="isApplyingSkin || isSkinManagementReadOnly"
|
||||
:aria-label="formatMessage(messages.applyButton)"
|
||||
@click="applySelectedSkin"
|
||||
>
|
||||
<SpinnerIcon v-if="isApplyingSkin" class="animate-spin" />
|
||||
<CheckIcon v-else />
|
||||
<span class="skin-preview-action-label">
|
||||
{{ formatMessage(messages.applyButton) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="flex h-10 min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] border-0 bg-brand px-4 py-2.5 text-base font-semibold leading-5 text-[rgba(0,0,0,0.9)] shadow-md transition-[filter,transform] duration-200 enabled:hover:brightness-[--hover-brightness] enabled:focus-visible:brightness-[--hover-brightness] enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:size-5 [&>svg]:shrink-0"
|
||||
:disabled="isApplyingSkin || isSkinManagementReadOnly"
|
||||
@click="applySelectedSkin"
|
||||
v-else
|
||||
class="flex h-10 min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] border-0 bg-surface-4 px-4 py-2.5 text-base font-semibold leading-5 shadow-md transition-[filter,transform] duration-200 enabled:hover:brightness-[--hover-brightness] enabled:focus-visible:brightness-[--hover-brightness] enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:size-5 [&>svg]:shrink-0"
|
||||
:disabled="!selectedSkin || isSkinManagementReadOnly"
|
||||
@click="(e: MouseEvent) => selectedSkin && editSkinModal?.show(e, selectedSkin)"
|
||||
>
|
||||
<SpinnerIcon v-if="isApplyingSkin" class="animate-spin" />
|
||||
<CheckIcon v-else />
|
||||
{{ formatMessage(messages.applyButton) }}
|
||||
<EditIcon />
|
||||
{{ formatMessage(messages.editSkinButton) }}
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="selectedSkinHasEarsFeatures"
|
||||
class="ears-feature-notice box-border flex w-full max-w-[340px] items-center justify-center gap-1.5 px-2"
|
||||
>
|
||||
<div class="ears-feature-copy flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<img
|
||||
:src="EarsModIcon"
|
||||
alt=""
|
||||
class="size-10 shrink-0 rounded-[7px] border border-solid border-surface-5 object-cover"
|
||||
/>
|
||||
<p
|
||||
class="ears-feature-description m-0 min-w-0 flex-1 text-sm font-medium leading-5 text-primary"
|
||||
>
|
||||
{{ earsFeatureNoticeParts.before
|
||||
}}<router-link
|
||||
v-if="earsFeatureNoticeParts.hasEarsLink"
|
||||
to="/project/mfzaZK3Z"
|
||||
class="text-inherit underline"
|
||||
>Ears</router-link
|
||||
>{{ earsFeatureNoticeParts.after }}
|
||||
</p>
|
||||
<router-link
|
||||
to="/project/mfzaZK3Z"
|
||||
class="ears-feature-compact-label hidden min-w-0 flex-1 text-sm font-medium leading-5 text-primary underline"
|
||||
>Ears</router-link
|
||||
>
|
||||
</div>
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="ears-feature-toggle-button !h-10 !rounded-[14px] !px-4 shadow-md"
|
||||
:aria-pressed="earsFeaturesEnabled"
|
||||
:aria-label="
|
||||
formatMessage(
|
||||
earsFeaturesEnabled
|
||||
? messages.toggleEarsFeaturesOff
|
||||
: messages.toggleEarsFeaturesOn,
|
||||
)
|
||||
"
|
||||
@click="earsFeaturesEnabled = !earsFeaturesEnabled"
|
||||
>
|
||||
{{
|
||||
formatMessage(
|
||||
earsFeaturesEnabled
|
||||
? messages.toggleEarsFeaturesOff
|
||||
: messages.toggleEarsFeaturesOn,
|
||||
)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Toggle
|
||||
v-model="earsFeaturesEnabled"
|
||||
v-tooltip="
|
||||
formatMessage(
|
||||
earsFeaturesEnabled
|
||||
? messages.toggleEarsFeaturesOff
|
||||
: messages.toggleEarsFeaturesOn,
|
||||
)
|
||||
"
|
||||
small
|
||||
class="ears-feature-toggle-switch"
|
||||
:aria-label="
|
||||
formatMessage(
|
||||
earsFeaturesEnabled
|
||||
? messages.toggleEarsFeaturesOff
|
||||
: messages.toggleEarsFeaturesOn,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
class="flex h-10 min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[14px] border-0 bg-surface-4 px-4 py-2.5 text-base font-semibold leading-5 shadow-md transition-[filter,transform] duration-200 enabled:hover:brightness-[--hover-brightness] enabled:focus-visible:brightness-[--hover-brightness] enabled:active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:size-5 [&>svg]:shrink-0"
|
||||
:disabled="!selectedSkin || isSkinManagementReadOnly"
|
||||
@click="(e: MouseEvent) => selectedSkin && editSkinModal?.show(e, selectedSkin)"
|
||||
>
|
||||
<EditIcon />
|
||||
{{ formatMessage(messages.editSkinButton) }}
|
||||
</button>
|
||||
</template>
|
||||
</SkinPreviewRenderer>
|
||||
</div>
|
||||
@@ -1147,6 +1284,51 @@ await loadSkins()
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.skin-preview-subtitle {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.ears-feature-toggle-switch {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container (max-width: 300px) {
|
||||
.skin-preview-subtitle {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.has-ears-features .skin-preview-action-button {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.has-ears-features .skin-preview-action-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ears-feature-toggle-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ears-feature-notice {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.ears-feature-description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ears-feature-compact-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ears-feature-toggle-switch {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.skin-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr);
|
||||
|
||||
@@ -13,207 +13,31 @@
|
||||
@unlinked="fetchInstance"
|
||||
/>
|
||||
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar
|
||||
:src="icon ? icon : undefined"
|
||||
:alt="instance.name"
|
||||
size="64px"
|
||||
:tint-by="instance.id"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ instance.name }}
|
||||
</template>
|
||||
<template #stats>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<template v-if="!isServerInstance">
|
||||
<div class="flex items-center gap-2 capitalize font-medium">
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</div>
|
||||
|
||||
<template v-if="showInstancePlayTime">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
|
||||
|
||||
<div class="flex items-center gap-2 font-medium">
|
||||
<template v-if="timePlayed > 0">
|
||||
{{ timePlayedHumanized }}
|
||||
</template>
|
||||
<template v-else> Never played </template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-if="loadingServerPing">
|
||||
<ServerOnlinePlayers
|
||||
v-if="playersOnline !== undefined"
|
||||
:online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
hide-label
|
||||
/>
|
||||
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
|
||||
<div
|
||||
v-if="
|
||||
(playersOnline !== undefined || recentPlays !== undefined) &&
|
||||
(minecraftServer?.region || ping)
|
||||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
<ServerPing v-if="ping" :ping="ping" />
|
||||
</template>
|
||||
|
||||
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
|
||||
|
||||
<div
|
||||
v-if="minecraftServer?.region || ping"
|
||||
class="w-1.5 h-1.5 rounded-full bg-surface-5"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="linkedProjectV3"
|
||||
class="flex gap-1.5 items-center font-medium text-primary"
|
||||
>
|
||||
Linked to
|
||||
<Avatar
|
||||
:src="linkedProjectV3.icon_url"
|
||||
:alt="linkedProjectV3.name"
|
||||
:tint-by="instance.id"
|
||||
size="24px"
|
||||
/>
|
||||
<router-link
|
||||
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
|
||||
class="hover:underline text-primary truncate"
|
||||
>
|
||||
{{ linkedProjectV3.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="
|
||||
[
|
||||
'installing',
|
||||
'pack_installing',
|
||||
'pack_installed',
|
||||
'not_installed',
|
||||
'minecraft_installing',
|
||||
].includes(instance.install_stage)
|
||||
"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button disabled>Installing...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="instance.install_stage !== 'installed'"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button @click="repairInstance()">
|
||||
<DownloadIcon />
|
||||
Repair
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing === true" color="red" size="large">
|
||||
<button :disabled="stopping" @click="stopInstance('InstancePage')">
|
||||
<StopCircleIcon />
|
||||
{{ stopping ? 'Stopping...' : 'Stop' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="playing === false && loading === false && !isServerInstance"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button @click="startInstance('InstancePage')">
|
||||
<PlayIcon />
|
||||
Play
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div
|
||||
v-else-if="playing === false && loading === false && isServerInstance"
|
||||
class="joined-buttons"
|
||||
>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<button @click="handlePlayServer()">
|
||||
<PlayIcon />
|
||||
Play
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'join_server',
|
||||
action: () => handlePlayServer(),
|
||||
},
|
||||
{
|
||||
id: 'launch_instance',
|
||||
action: () => startInstance('InstancePage'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="w-0 text-xl relative top-0.5 right-2.5">
|
||||
<DropdownIcon />
|
||||
</div>
|
||||
|
||||
<template #join_server>
|
||||
<PlayIcon />
|
||||
Join server
|
||||
</template>
|
||||
<template #launch_instance>
|
||||
<PlayIcon />
|
||||
Launch instance
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
v-else-if="loading === true && playing === false"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button disabled>Starting...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large">
|
||||
<button v-tooltip="'Instance settings'" @click="settingsModal?.show()">
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent" circular size="large">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => {
|
||||
if (instance) showInstanceInFolder(instance.id)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
action: () => exportModal?.show(),
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #share-instance> <UserPlusIcon /> Share instance </template>
|
||||
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||
<template #create-shortcut> <ExternalIcon /> Create shortcut </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
<InstancePageHeader
|
||||
:instance="instance"
|
||||
:icon-src="icon"
|
||||
:is-server-instance="isServerInstance"
|
||||
:show-instance-play-time="showInstancePlayTime"
|
||||
:time-played="timePlayed"
|
||||
:playing="playing"
|
||||
:loading="loading"
|
||||
:stopping="stopping"
|
||||
:loading-server-ping="loadingServerPing"
|
||||
:players-online="playersOnline"
|
||||
:status-online="statusOnline"
|
||||
:recent-plays="recentPlays"
|
||||
:ping="ping"
|
||||
:minecraft-server="minecraftServer"
|
||||
:linked-project-v3="linkedProjectV3"
|
||||
@repair="() => repairInstance()"
|
||||
@stop="() => stopInstance('InstancePage')"
|
||||
@play="() => startInstance('InstancePage')"
|
||||
@play-server="() => handlePlayServer()"
|
||||
@settings="() => settingsModal?.show()"
|
||||
@open-folder="() => instance && showInstanceInFolder(instance.id)"
|
||||
@export="() => exportModal?.show()"
|
||||
@create-shortcut="() => createShortcut()"
|
||||
/>
|
||||
</div>
|
||||
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
|
||||
<NavTabs :links="tabs" />
|
||||
@@ -272,49 +96,30 @@ import {
|
||||
BoxesIcon,
|
||||
CheckCircleIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
DropdownIcon,
|
||||
EditIcon,
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
FolderOpenIcon,
|
||||
GlobeIcon,
|
||||
HashIcon,
|
||||
MoreVerticalIcon,
|
||||
PackageIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
ServerIcon,
|
||||
SettingsIcon,
|
||||
StopCircleIcon,
|
||||
TerminalSquareIcon,
|
||||
UpdatedIcon,
|
||||
UserPlusIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
ContentPageHeader,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
ServerOnlinePlayers,
|
||||
ServerPing,
|
||||
ServerRecentPlays,
|
||||
ServerRegion,
|
||||
useLoadingBarToken,
|
||||
} from '@modrinth/ui'
|
||||
import { injectNotificationManager, NavTabs, useLoadingBarToken } from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import InstancePageHeader from '@/components/ui/instance-page-header/index.vue'
|
||||
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||
import {
|
||||
@@ -336,7 +141,6 @@ import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { useBreadcrumbs, useTheming } from '@/store/state'
|
||||
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const { addNotification, handleError } = injectNotificationManager()
|
||||
@@ -758,22 +562,6 @@ const timePlayed = computed(() => {
|
||||
: 0
|
||||
})
|
||||
|
||||
const timePlayedHumanized = computed(() => {
|
||||
const duration = dayjs.duration(timePlayed.value, 'seconds')
|
||||
const hours = Math.floor(duration.asHours())
|
||||
if (hours >= 1) {
|
||||
return hours + ' hour' + (hours > 1 ? 's' : '')
|
||||
}
|
||||
|
||||
const minutes = Math.floor(duration.asMinutes())
|
||||
if (minutes >= 1) {
|
||||
return minutes + ' minute' + (minutes > 1 ? 's' : '')
|
||||
}
|
||||
|
||||
const seconds = Math.floor(duration.asSeconds())
|
||||
return seconds + ' second' + (seconds > 1 ? 's' : '')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenInstances()
|
||||
|
||||
@@ -59,142 +59,100 @@
|
||||
>
|
||||
<ProjectBackgroundGradient :project="data" />
|
||||
</Teleport>
|
||||
<ProjectHeader
|
||||
<ProjectPageHeader
|
||||
v-else
|
||||
:project="data"
|
||||
:project-v3="projectV3"
|
||||
:ping="serverPing"
|
||||
:show-status-badge="data.status !== 'approved'"
|
||||
@contextmenu.prevent.stop="handleRightClick"
|
||||
@category="(category) => router.push(`${projectSearchUrl}?f=categories:${category}`)"
|
||||
>
|
||||
<template v-if="isServerProject" #actions>
|
||||
<ButtonStyled v-if="serverPlaying" size="large" color="red">
|
||||
<button @click="handleStopServer">
|
||||
<StopCircleIcon />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else size="large" color="brand">
|
||||
<button
|
||||
:disabled="data && installingServerProjects.includes(data.id)"
|
||||
@click="handleClickPlay"
|
||||
>
|
||||
<PlayIcon />
|
||||
{{
|
||||
data && installingServerProjects.includes(data.id)
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
: formatMessage(commonMessages.playButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
|
||||
@click="handleAddServerToInstance"
|
||||
>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/project/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser>
|
||||
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
|
||||
</template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<template #actions>
|
||||
<template v-if="isServerProject">
|
||||
<ButtonStyled v-if="serverPlaying" color="red" size="large">
|
||||
<button type="button" @click="handleStopServer">
|
||||
<StopCircleIcon />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="brand" size="large">
|
||||
<button type="button" :disabled="serverInstallLoading" @click="handleClickPlay">
|
||||
<PlayIcon />
|
||||
{{
|
||||
serverInstallLoading
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
: formatMessage(commonMessages.playButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large">
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
|
||||
type="button"
|
||||
:aria-label="formatMessage(commonMessages.addServerToInstanceButton)"
|
||||
@click="handleAddServerToInstance"
|
||||
>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="serverProjectHeaderMoreActions"
|
||||
tooltip="More options"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
|
||||
<button v-tooltip="formatMessage(messages.alreadyInstalled)" type="button" disabled>
|
||||
<CheckIcon />
|
||||
{{ formatMessage(commonMessages.installedLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="showSwitchVersion" size="large">
|
||||
<button type="button" @click="goToVersions">
|
||||
<SwapIcon />
|
||||
{{ formatMessage(messages.switchVersion) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="brand" size="large">
|
||||
<button
|
||||
v-tooltip="
|
||||
installButtonInstalled ? formatMessage(messages.alreadyInstalled) : undefined
|
||||
"
|
||||
type="button"
|
||||
:disabled="installButtonDisabled"
|
||||
@click="install(null)"
|
||||
>
|
||||
<component :is="installButtonIcon" :class="installButtonIconClass" />
|
||||
{{
|
||||
installButtonInstalled
|
||||
? formatMessage(commonMessages.installedLabel)
|
||||
: installButtonValidating
|
||||
? formatMessage(commonMessages.validatingLabel)
|
||||
: installButtonLoading
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
: serverProjectSelected
|
||||
? formatMessage(commonMessages.selectedLabel)
|
||||
: formatMessage(commonMessages.installButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="projectHeaderMoreActions"
|
||||
tooltip="More options"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else #actions>
|
||||
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
|
||||
<button v-tooltip="installButtonTooltip" disabled>
|
||||
<CheckIcon />
|
||||
{{ formatMessage(commonMessages.installedLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="showSwitchVersion" size="large">
|
||||
<button @click="goToVersions">
|
||||
<SwapIcon />
|
||||
{{ formatMessage(messages.switchVersion) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installButtonTooltip"
|
||||
:disabled="installButtonDisabled"
|
||||
@click="install(null)"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="installButtonLoading && !installButtonInstalled"
|
||||
class="animate-spin"
|
||||
/>
|
||||
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
|
||||
<CheckIcon v-else />
|
||||
{{ installButtonLabel }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'follow',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser>
|
||||
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
|
||||
</template>
|
||||
<template #follow> <HeartIcon /> Follow </template>
|
||||
<template #save> <BookmarkIcon /> Save </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectHeader>
|
||||
</ProjectPageHeader>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{
|
||||
@@ -289,9 +247,8 @@ import {
|
||||
getTargetInstallPreferences,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
ProjectBackgroundGradient,
|
||||
ProjectHeader,
|
||||
ProjectPageHeader,
|
||||
ProjectSidebarCompatibility,
|
||||
ProjectSidebarCreators,
|
||||
ProjectSidebarDetails,
|
||||
@@ -300,6 +257,7 @@ import {
|
||||
ProjectSidebarTags,
|
||||
requestInstall,
|
||||
SelectedProjectsFloatingBar,
|
||||
TeleportOverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
@@ -358,14 +316,14 @@ const messages = defineMessages({
|
||||
id: 'app.project.install-context.back-to-browse',
|
||||
defaultMessage: 'Back to discover',
|
||||
},
|
||||
installContentToInstance: {
|
||||
id: 'app.project.install-context.install-content-to-instance',
|
||||
defaultMessage: 'Install content to instance',
|
||||
},
|
||||
alreadyInstalled: {
|
||||
id: 'app.project.install-button.already-installed',
|
||||
defaultMessage: 'This project is already installed',
|
||||
},
|
||||
installContentToInstance: {
|
||||
id: 'app.project.install-context.install-content-to-instance',
|
||||
defaultMessage: 'Install content to instance',
|
||||
},
|
||||
switchVersion: {
|
||||
id: 'app.project.install-button.switch-version',
|
||||
defaultMessage: 'Switch version',
|
||||
@@ -529,17 +487,72 @@ const installButtonInstalled = computed(() =>
|
||||
const installButtonDisabled = computed(
|
||||
() => installButtonInstalled.value || installButtonLoading.value,
|
||||
)
|
||||
const installButtonLabel = computed(() => {
|
||||
if (installButtonInstalled.value) return formatMessage(commonMessages.installedLabel)
|
||||
if (installButtonValidating.value) return formatMessage(commonMessages.validatingLabel)
|
||||
if (installButtonLoading.value) return formatMessage(commonMessages.installingLabel)
|
||||
if (serverProjectSelected.value) return formatMessage(commonMessages.selectedLabel)
|
||||
return formatMessage(commonMessages.installButton)
|
||||
})
|
||||
const installButtonTooltip = computed(() => {
|
||||
if (installButtonInstalled.value) return formatMessage(messages.alreadyInstalled)
|
||||
return null
|
||||
const serverInstallLoading = computed(
|
||||
() => !!data.value && installingServerProjects.value.includes(data.value.id),
|
||||
)
|
||||
const installButtonIcon = computed(() => {
|
||||
if (installButtonLoading.value && !installButtonInstalled.value) return SpinnerIcon
|
||||
if (!installButtonInstalled.value && !serverProjectSelected.value) return DownloadIcon
|
||||
return CheckIcon
|
||||
})
|
||||
const installButtonIconClass = computed(() =>
|
||||
installButtonLoading.value && !installButtonInstalled.value ? 'animate-spin' : undefined,
|
||||
)
|
||||
const serverProjectHeaderMoreActions = computed(() => [
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
label: formatMessage(commonMessages.openInModrinthButton),
|
||||
icon: ExternalIcon,
|
||||
action: openProjectInBrowser,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: formatMessage(commonMessages.reportButton),
|
||||
icon: ReportIcon,
|
||||
color: 'red',
|
||||
action: reportProject,
|
||||
},
|
||||
])
|
||||
const projectHeaderMoreActions = computed(() => [
|
||||
{
|
||||
id: 'follow',
|
||||
label: formatMessage(commonMessages.followButton),
|
||||
icon: HeartIcon,
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
label: formatMessage(commonMessages.saveButton),
|
||||
icon: BookmarkIcon,
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
label: formatMessage(commonMessages.openInModrinthButton),
|
||||
icon: ExternalIcon,
|
||||
action: openProjectInBrowser,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: formatMessage(commonMessages.reportButton),
|
||||
icon: ReportIcon,
|
||||
color: 'red',
|
||||
action: reportProject,
|
||||
},
|
||||
])
|
||||
const projectSearchUrl = computed(
|
||||
() => `/browse/${isServerProject.value ? 'server' : data.value?.project_type}`,
|
||||
)
|
||||
|
||||
const showSwitchVersion = computed(() => !!instance.value && installed.value)
|
||||
const onVersionsPage = computed(() => route.name === 'Versions')
|
||||
@@ -585,6 +598,17 @@ function handleAddServerToInstance() {
|
||||
showAddServerToInstanceModal(data.value.title, address)
|
||||
}
|
||||
|
||||
function openProjectInBrowser() {
|
||||
if (!data.value) return
|
||||
const type = isServerProject.value ? 'project' : data.value.project_type
|
||||
void openUrl(`https://modrinth.com/${type}/${data.value.slug}`)
|
||||
}
|
||||
|
||||
function reportProject() {
|
||||
if (!data.value) return
|
||||
void openUrl(`https://modrinth.com/report?item=project&itemID=${data.value.id}`)
|
||||
}
|
||||
|
||||
async function fetchProjectData() {
|
||||
const [project, projectV3Result] = await Promise.all([
|
||||
get_project(route.params.id, 'must_revalidate').catch(handleError),
|
||||
|
||||
@@ -279,7 +279,12 @@ fn main() {
|
||||
"scroll_ads_window",
|
||||
"show_ads_window",
|
||||
"show_ads_consent_overlay",
|
||||
"show_ads_consent_preferences",
|
||||
"open_ads_consent_preferences",
|
||||
"hide_ads_consent_preferences",
|
||||
"hide_ads_consent_overlay",
|
||||
"get_ads_consent_required",
|
||||
"perform_ads_consent_action",
|
||||
"record_ads_click",
|
||||
"open_link",
|
||||
"get_ads_personalization",
|
||||
|
||||
@@ -6,6 +6,14 @@ document.addEventListener(
|
||||
window.top.postMessage({ modrinthAdClick: true }, MODRINTH_ORIGIN)
|
||||
|
||||
let target = e.target
|
||||
if (target?.closest?.('.qc-cmp2-close-icon')) {
|
||||
if (modrinthAdsConsentReprompt) {
|
||||
setTimeout(finishAdsConsentReprompt)
|
||||
} else if (document.documentElement.classList.contains('modrinth-ads-consent-preferences')) {
|
||||
setTimeout(() => void restoreAdsConsentNotification())
|
||||
}
|
||||
}
|
||||
|
||||
while (target != null) {
|
||||
if (target.matches('a')) {
|
||||
e.preventDefault()
|
||||
@@ -27,6 +35,25 @@ window.open = (url, target, features) => {
|
||||
let modrinthAdsConsentOverlayShown = false
|
||||
let modrinthTcfListenerInstalled = false
|
||||
let modrinthTcfListenerAttempts = 0
|
||||
let modrinthAdsConsentReprompt = false
|
||||
let modrinthAdsConsentRepromptManaging = false
|
||||
let modrinthAdsConsentActionRequestId = 0
|
||||
const modrinthAdsConsentActionResolvers = new Map()
|
||||
|
||||
function installAdsRailStyle() {
|
||||
if (document.getElementById('modrinth-ads-rail-style')) {
|
||||
return
|
||||
}
|
||||
const style = document.createElement('style')
|
||||
style.id = 'modrinth-ads-rail-style'
|
||||
style.textContent = `
|
||||
html.modrinth-ads-consent-preferences #modrinth-rail-1 {
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`
|
||||
document.documentElement.appendChild(style)
|
||||
}
|
||||
|
||||
function installAdsConsentOverlayStyle() {
|
||||
if (document.getElementById('modrinth-ads-consent-overlay-style')) {
|
||||
@@ -35,10 +62,16 @@ function installAdsConsentOverlayStyle() {
|
||||
const style = document.createElement('style')
|
||||
style.id = 'modrinth-ads-consent-overlay-style'
|
||||
style.textContent = `
|
||||
html.modrinth-ads-consent-overlay #modrinth-rail-1 {
|
||||
html.modrinth-ads-consent-overlay:not(.modrinth-ads-consent-preferences) #modrinth-rail-1 {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
html.modrinth-ads-consent-overlay:not(.modrinth-ads-consent-preferences) #qc-cmp2-container,
|
||||
html.modrinth-ads-consent-overlay:not(.modrinth-ads-consent-preferences) #qc-cmp2-main {
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-close-icon {
|
||||
background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M.5.5l23 23m0-23l-23 23' fill='none' stroke='%23b0bac5' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3C/svg%3E") 0% 0% / 66% auto no-repeat !important;
|
||||
}
|
||||
@@ -63,12 +96,243 @@ function invokeAdsConsentOverlayCommand(shown) {
|
||||
invoke(`plugin:ads|${command}`, args).catch(() => {})
|
||||
}
|
||||
|
||||
function revealAdsConsentPreferences() {
|
||||
document.documentElement.classList.add('modrinth-ads-consent-preferences')
|
||||
document.getElementById('modrinth-ads-consent-overlay-style')?.remove()
|
||||
}
|
||||
|
||||
function concealAdsConsentPreferences() {
|
||||
document.documentElement.classList.remove('modrinth-ads-consent-preferences')
|
||||
installAdsConsentOverlayStyle()
|
||||
}
|
||||
|
||||
async function expandAdsConsentWebview() {
|
||||
const invoke = getTauriInvoke()
|
||||
if (typeof invoke !== 'function') {
|
||||
throw new Error('Tauri invoke is unavailable in the ads webview')
|
||||
}
|
||||
|
||||
await invoke('plugin:ads|show_ads_consent_preferences')
|
||||
}
|
||||
|
||||
function sendAdsConsentCommandToChildFrames(command) {
|
||||
document.querySelectorAll('iframe').forEach((frame) => {
|
||||
frame.contentWindow?.postMessage({ modrinthAdsConsentCommand: command }, '*')
|
||||
})
|
||||
}
|
||||
|
||||
function isDirectChildFrame(source) {
|
||||
return Array.from(document.querySelectorAll('iframe')).some(
|
||||
(frame) => frame.contentWindow === source,
|
||||
)
|
||||
}
|
||||
|
||||
function findAdsConsentButton(action) {
|
||||
const container = document.querySelector('#qc-cmp2-container, #qc-cmp2-main')
|
||||
if (!container) return null
|
||||
const summaryButtons = Array.from(container.querySelectorAll('.qc-cmp2-summary-buttons button'))
|
||||
|
||||
if (action === 'accept') {
|
||||
const explicitAcceptButton = container.querySelector('#accept-btn')
|
||||
if (explicitAcceptButton) return explicitAcceptButton
|
||||
if (summaryButtons.length >= 3) return summaryButtons[2]
|
||||
|
||||
return container.querySelector('.qc-cmp2-summary-buttons button[mode="primary"]')
|
||||
}
|
||||
|
||||
if (action === 'reject') {
|
||||
const explicitRejectButton = container.querySelector('#disagree-btn')
|
||||
if (explicitRejectButton) return explicitRejectButton
|
||||
if (summaryButtons.length >= 3) return summaryButtons[1]
|
||||
|
||||
const secondaryButtons = container.querySelectorAll(
|
||||
'.qc-cmp2-summary-buttons button[mode="secondary"]',
|
||||
)
|
||||
if (secondaryButtons.length > 1) return secondaryButtons[1]
|
||||
|
||||
return summaryButtons.find(
|
||||
(button) => button.textContent?.trim().toLowerCase() === 'reject all',
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
container.querySelector(
|
||||
'#more-options-btn, .qc-cmp2-summary-buttons > button[mode="secondary"]:first-of-type',
|
||||
) ?? summaryButtons[0]
|
||||
)
|
||||
}
|
||||
|
||||
function clickAdsConsentButtonWhenReady(action, timeoutMs, onButtonFound) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
|
||||
return new Promise((resolve) => {
|
||||
function tryClick() {
|
||||
const button = findAdsConsentButton(action)
|
||||
if (button) {
|
||||
// CMP navigation can replace this document during the click, so acknowledge it first.
|
||||
onButtonFound?.()
|
||||
resolve(true)
|
||||
button.click()
|
||||
} else if (Date.now() >= deadline) {
|
||||
resolve(false)
|
||||
} else {
|
||||
setTimeout(tryClick, 50)
|
||||
}
|
||||
}
|
||||
|
||||
tryClick()
|
||||
})
|
||||
}
|
||||
|
||||
function performAdsConsentActionAcrossFrames(action, timeoutMs) {
|
||||
const requestId = `${Date.now()}-${++modrinthAdsConsentActionRequestId}`
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false
|
||||
const settle = (clicked) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timeout)
|
||||
modrinthAdsConsentActionResolvers.delete(requestId)
|
||||
resolve(clicked)
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => settle(false), timeoutMs)
|
||||
modrinthAdsConsentActionResolvers.set(requestId, () => settle(true))
|
||||
sendAdsConsentCommandToChildFrames({ type: 'perform', action, requestId, timeoutMs })
|
||||
clickAdsConsentButtonWhenReady(action, timeoutMs, () => settle(true)).then((clicked) => {
|
||||
if (!clicked) settle(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function waitForAdsConsentLayout() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
async function restoreAdsConsentNotification() {
|
||||
concealAdsConsentPreferences()
|
||||
sendAdsConsentCommandToChildFrames({ type: 'conceal' })
|
||||
|
||||
const invoke = getTauriInvoke()
|
||||
if (typeof invoke === 'function') {
|
||||
await invoke('plugin:ads|hide_ads_consent_preferences')
|
||||
}
|
||||
}
|
||||
|
||||
function finishAdsConsentReprompt() {
|
||||
modrinthAdsConsentReprompt = false
|
||||
modrinthAdsConsentRepromptManaging = false
|
||||
modrinthAdsConsentOverlayShown = false
|
||||
document.documentElement.classList.remove('modrinth-ads-consent-overlay')
|
||||
concealAdsConsentPreferences()
|
||||
sendAdsConsentCommandToChildFrames({ type: 'conceal' })
|
||||
invokeAdsConsentOverlayCommand(false)
|
||||
}
|
||||
|
||||
async function openAdsConsentPreferences() {
|
||||
revealAdsConsentPreferences()
|
||||
sendAdsConsentCommandToChildFrames({ type: 'reveal' })
|
||||
await expandAdsConsentWebview()
|
||||
await waitForAdsConsentLayout()
|
||||
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
sendAdsConsentCommandToChildFrames({ type: 'resize' })
|
||||
|
||||
const clicked = await performAdsConsentActionAcrossFrames('manage', 2500)
|
||||
if (!clicked) {
|
||||
await restoreAdsConsentNotification()
|
||||
}
|
||||
}
|
||||
|
||||
async function performAdsConsentAction(action) {
|
||||
if (!['accept', 'reject', 'manage'].includes(action)) return
|
||||
|
||||
if (action === 'manage') {
|
||||
try {
|
||||
await openAdsConsentPreferences()
|
||||
} catch {
|
||||
await restoreAdsConsentNotification()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await performAdsConsentActionAcrossFrames(action, 1000)
|
||||
}
|
||||
|
||||
window.modrinthAdsConsentAction = (action) => {
|
||||
void performAdsConsentAction(action)
|
||||
}
|
||||
|
||||
window.modrinthAdsReopenConsentPreferences = async () => {
|
||||
modrinthAdsConsentReprompt = true
|
||||
modrinthAdsConsentRepromptManaging = false
|
||||
revealAdsConsentPreferences()
|
||||
sendAdsConsentCommandToChildFrames({ type: 'reveal' })
|
||||
|
||||
try {
|
||||
await expandAdsConsentWebview()
|
||||
await waitForAdsConsentLayout()
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
sendAdsConsentCommandToChildFrames({ type: 'resize' })
|
||||
|
||||
if (typeof window.__tcfapi === 'function') {
|
||||
window.__tcfapi('displayConsentUi', 2, () => {})
|
||||
} else {
|
||||
finishAdsConsentReprompt()
|
||||
}
|
||||
} catch {
|
||||
finishAdsConsentReprompt()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const resultRequestId = event.data?.modrinthAdsConsentResult
|
||||
if (typeof resultRequestId === 'string' && isDirectChildFrame(event.source)) {
|
||||
if (window.top === window) {
|
||||
modrinthAdsConsentActionResolvers.get(resultRequestId)?.()
|
||||
} else {
|
||||
window.parent.postMessage({ modrinthAdsConsentResult: resultRequestId }, '*')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (window.top === window || event.source !== window.parent) return
|
||||
|
||||
const command = event.data?.modrinthAdsConsentCommand
|
||||
if (!command || typeof command !== 'object') return
|
||||
|
||||
if (command.type === 'reveal') {
|
||||
revealAdsConsentPreferences()
|
||||
sendAdsConsentCommandToChildFrames(command)
|
||||
} else if (command.type === 'conceal') {
|
||||
concealAdsConsentPreferences()
|
||||
sendAdsConsentCommandToChildFrames(command)
|
||||
} else if (command.type === 'resize') {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
sendAdsConsentCommandToChildFrames(command)
|
||||
} else if (
|
||||
command.type === 'perform' &&
|
||||
typeof command.action === 'string' &&
|
||||
typeof command.requestId === 'string' &&
|
||||
typeof command.timeoutMs === 'number'
|
||||
) {
|
||||
sendAdsConsentCommandToChildFrames(command)
|
||||
clickAdsConsentButtonWhenReady(command.action, command.timeoutMs, () => {
|
||||
window.parent.postMessage({ modrinthAdsConsentResult: command.requestId }, '*')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function setAdsConsentOverlay(shown) {
|
||||
if (modrinthAdsConsentOverlayShown === shown) return
|
||||
|
||||
modrinthAdsConsentOverlayShown = shown
|
||||
installAdsConsentOverlayStyle()
|
||||
document.documentElement.classList.toggle('modrinth-ads-consent-overlay', shown)
|
||||
if (!shown) {
|
||||
document.documentElement.classList.remove('modrinth-ads-consent-preferences')
|
||||
}
|
||||
|
||||
if (window.top === window) {
|
||||
invokeAdsConsentOverlayCommand(shown)
|
||||
@@ -92,8 +356,23 @@ function handleTcfConsentEvent(tcData, success) {
|
||||
if (!success || !tcData) return
|
||||
|
||||
if (tcData.eventStatus === 'cmpuishown') {
|
||||
if (modrinthAdsConsentReprompt) {
|
||||
if (!modrinthAdsConsentRepromptManaging) {
|
||||
modrinthAdsConsentRepromptManaging = true
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
sendAdsConsentCommandToChildFrames({ type: 'resize' })
|
||||
void performAdsConsentActionAcrossFrames('manage', 2500)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setAdsConsentOverlay(true)
|
||||
} else if (tcData.eventStatus === 'useractioncomplete' || tcData.eventStatus === 'tcloaded') {
|
||||
} else if (tcData.eventStatus === 'useractioncomplete') {
|
||||
if (modrinthAdsConsentReprompt) {
|
||||
finishAdsConsentReprompt()
|
||||
return
|
||||
}
|
||||
|
||||
setAdsConsentOverlay(false)
|
||||
}
|
||||
}
|
||||
@@ -190,6 +469,7 @@ function muteVideos() {
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
installAdsRailStyle()
|
||||
installAdsConsentOverlayStyle()
|
||||
muteVideos()
|
||||
muteAudioContext()
|
||||
|
||||
+170
-4
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{Manager, PhysicalPosition, PhysicalSize, Runtime};
|
||||
use tauri::{Emitter, Manager, PhysicalPosition, PhysicalSize, Rect, Runtime};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use theseus::settings;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -11,6 +11,7 @@ use tokio::sync::RwLock;
|
||||
pub struct AdsState {
|
||||
pub shown: bool,
|
||||
pub modal_shown: bool,
|
||||
pub consent_required: bool,
|
||||
pub consent_overlay_shown: bool,
|
||||
pub occluded: bool,
|
||||
pub last_click: Option<Instant>,
|
||||
@@ -18,6 +19,7 @@ pub struct AdsState {
|
||||
}
|
||||
|
||||
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
|
||||
const ADS_CONSENT_REQUIRED_EVENT: &str = "ads-consent-required";
|
||||
const APP_TITLE_BAR_HEIGHT: f32 = 48.0;
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
pub(super) const OCCLUDED_AREA_THRESHOLD: f64 = 0.5;
|
||||
@@ -275,6 +277,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
app.manage(RwLock::new(AdsState {
|
||||
shown: true,
|
||||
modal_shown: false,
|
||||
consent_required: false,
|
||||
consent_overlay_shown: false,
|
||||
occluded: false,
|
||||
last_click: None,
|
||||
@@ -293,6 +296,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
.map(|state| {
|
||||
state.shown
|
||||
&& !state.modal_shown
|
||||
&& !state.consent_required
|
||||
&& !state.consent_overlay_shown
|
||||
&& !state.occluded
|
||||
})
|
||||
@@ -358,7 +362,12 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
hide_ads_window,
|
||||
show_ads_window,
|
||||
show_ads_consent_overlay,
|
||||
show_ads_consent_preferences,
|
||||
open_ads_consent_preferences,
|
||||
hide_ads_consent_preferences,
|
||||
hide_ads_consent_overlay,
|
||||
get_ads_consent_required,
|
||||
perform_ads_consent_action,
|
||||
record_ads_click,
|
||||
open_link,
|
||||
get_ads_personalization,
|
||||
@@ -635,6 +644,10 @@ pub async fn init_ads_window<R: Runtime>(
|
||||
// });
|
||||
}
|
||||
|
||||
if state.shown && state.consent_required {
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -648,6 +661,8 @@ pub async fn show_ads_window<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
dpr: f32,
|
||||
) -> crate::api::Result<()> {
|
||||
let mut consent_required = false;
|
||||
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
@@ -667,6 +682,12 @@ pub async fn show_ads_window<R: Runtime>(
|
||||
webview.show().ok();
|
||||
set_webview_visible_for_window(&app, webview, true);
|
||||
}
|
||||
|
||||
consent_required = state.shown && state.consent_required;
|
||||
}
|
||||
|
||||
if consent_required {
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -677,11 +698,13 @@ pub async fn hide_ads_window<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
reset: Option<bool>,
|
||||
) -> crate::api::Result<()> {
|
||||
let reset = reset.unwrap_or(false);
|
||||
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
|
||||
if reset.unwrap_or(false) {
|
||||
if reset {
|
||||
state.shown = false;
|
||||
state.consent_overlay_shown = false;
|
||||
} else {
|
||||
@@ -706,6 +729,10 @@ pub async fn hide_ads_window<R: Runtime>(
|
||||
webview.hide().ok();
|
||||
}
|
||||
|
||||
if reset {
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, false).ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -722,11 +749,45 @@ pub async fn show_ads_consent_overlay<R: Runtime>(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.consent_required = true;
|
||||
state.consent_overlay_shown = false;
|
||||
|
||||
if !state.modal_shown {
|
||||
let dpr = get_device_pixel_ratio(&app, None);
|
||||
let (position, size) = get_webview_position(&app, dpr)?;
|
||||
webview.set_size(size).ok();
|
||||
webview.set_position(position).ok();
|
||||
webview.show().ok();
|
||||
set_webview_visible_for_window(&app, webview, true);
|
||||
}
|
||||
}
|
||||
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn show_ads_consent_preferences<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> crate::api::Result<()> {
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
|
||||
if !state.consent_required {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.consent_overlay_shown = true;
|
||||
|
||||
let (position, size) = get_overlay_webview_position(&app)?;
|
||||
webview.set_size(size).ok();
|
||||
webview.set_position(position).ok();
|
||||
webview
|
||||
.set_bounds(Rect {
|
||||
position: position.into(),
|
||||
size: size.into(),
|
||||
})
|
||||
.ok();
|
||||
webview.show().ok();
|
||||
set_webview_visible_for_window(&app, webview, true);
|
||||
}
|
||||
@@ -734,6 +795,62 @@ pub async fn show_ads_consent_overlay<R: Runtime>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_ads_consent_preferences<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> crate::api::Result<()> {
|
||||
let Some(webview) = app.webviews().get("ads-window").cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
{
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
state.consent_required = true;
|
||||
state.consent_overlay_shown = false;
|
||||
}
|
||||
|
||||
webview.eval("window.modrinthAdsReopenConsentPreferences?.()")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restores the ad inventory bounds without resolving the pending consent request.
|
||||
#[tauri::command]
|
||||
pub async fn hide_ads_consent_preferences<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> crate::api::Result<()> {
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
|
||||
state.consent_overlay_shown = false;
|
||||
|
||||
if state.shown && !state.modal_shown {
|
||||
let dpr = get_device_pixel_ratio(&app, None);
|
||||
let (position, size) = get_webview_position(&app, dpr)?;
|
||||
|
||||
webview
|
||||
.set_bounds(Rect {
|
||||
position: position.into(),
|
||||
size: size.into(),
|
||||
})
|
||||
.ok();
|
||||
webview.show().ok();
|
||||
set_webview_visible_for_window(&app, webview, true);
|
||||
} else {
|
||||
webview
|
||||
.set_position(PhysicalPosition::new(-1000, -1000))
|
||||
.ok();
|
||||
webview.hide().ok();
|
||||
}
|
||||
}
|
||||
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hide_ads_consent_overlay<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
@@ -742,7 +859,9 @@ pub async fn hide_ads_consent_overlay<R: Runtime>(
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
let should_reload_ads = state.consent_required;
|
||||
|
||||
state.consent_required = false;
|
||||
state.consent_overlay_shown = false;
|
||||
|
||||
if state.shown && !state.modal_shown {
|
||||
@@ -759,6 +878,53 @@ pub async fn hide_ads_consent_overlay<R: Runtime>(
|
||||
.ok();
|
||||
webview.hide().ok();
|
||||
}
|
||||
|
||||
drop(state);
|
||||
|
||||
if should_reload_ads {
|
||||
webview.navigate(AD_LINK.parse().unwrap()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, false).ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_ads_consent_required<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> crate::api::Result<bool> {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let state = state.read().await;
|
||||
|
||||
Ok(state.shown && state.consent_required)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn perform_ads_consent_action<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
action: String,
|
||||
) -> crate::api::Result<()> {
|
||||
let script = match action.as_str() {
|
||||
"accept" => "window.modrinthAdsConsentAction?.('accept')",
|
||||
"reject" => "window.modrinthAdsConsentAction?.('reject')",
|
||||
"manage" => "window.modrinthAdsConsentAction?.('manage')",
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let should_perform = {
|
||||
let state = state.read().await;
|
||||
state.shown && state.consent_required
|
||||
};
|
||||
|
||||
if !should_perform {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
webview.eval(script)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -6,20 +6,27 @@ macro_rules! impl_cache_methods {
|
||||
$(
|
||||
paste::paste! {
|
||||
#[tauri::command]
|
||||
pub async fn [<get_ $variant:snake>](id: &str, cache_behaviour: Option<CacheBehaviour>) -> Result<Option<$type>>
|
||||
pub async fn [<get_ $variant:snake>](
|
||||
id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Option<$type>>
|
||||
{
|
||||
Ok(theseus::cache::[<get_ $variant:snake>](id, cache_behaviour).await?)
|
||||
let context = invocation_context;
|
||||
Ok(theseus::cache::[<get_ $variant:snake>](&context, id, cache_behaviour).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn [<get_ $variant:snake _many>](
|
||||
ids: Vec<String>,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<$type>>
|
||||
{
|
||||
let context = invocation_context;
|
||||
let ids = ids.iter().map(|x| &**x).collect::<Vec<&str>>();
|
||||
let entries =
|
||||
theseus::cache::[<get_ $variant:snake _many>](&*ids, cache_behaviour).await?;
|
||||
theseus::cache::[<get_ $variant:snake _many>](&context, &*ids, cache_behaviour).await?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
@@ -73,9 +80,13 @@ pub async fn purge_cache_types(cache_types: Vec<CacheValueType>) -> Result<()> {
|
||||
pub async fn get_project_versions(
|
||||
project_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Option<Vec<Version>>> {
|
||||
Ok(
|
||||
theseus::cache::get_project_versions(project_id, cache_behaviour)
|
||||
.await?,
|
||||
let context = invocation_context;
|
||||
Ok(theseus::cache::get_project_versions(
|
||||
&context,
|
||||
project_id,
|
||||
cache_behaviour,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn friends() -> crate::api::Result<Vec<UserFriend>> {
|
||||
Ok(theseus::friends::friends().await?)
|
||||
pub async fn friends(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> crate::api::Result<Vec<UserFriend>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::friends::friends(&context).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -23,11 +26,19 @@ pub async fn friend_statuses() -> crate::api::Result<Vec<UserStatus>> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_friend(user_id: &str) -> crate::api::Result<()> {
|
||||
Ok(theseus::friends::add_friend(user_id).await?)
|
||||
pub async fn add_friend(
|
||||
user_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> crate::api::Result<()> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::friends::add_friend(&context, user_id).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_friend(user_id: &str) -> crate::api::Result<()> {
|
||||
Ok(theseus::friends::remove_friend(user_id).await?)
|
||||
pub async fn remove_friend(
|
||||
user_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> crate::api::Result<()> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::friends::remove_friend(&context, user_id).await?)
|
||||
}
|
||||
|
||||
@@ -67,15 +67,23 @@ impl InstallPostInstallEditRequest {
|
||||
#[tauri::command]
|
||||
pub async fn install_get_modpack_preview(
|
||||
location: CreatePackLocation,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallModpackPreview> {
|
||||
Ok(theseus::pack::install_from::get_instance_from_pack(location).await?)
|
||||
let context = invocation_context;
|
||||
Ok(
|
||||
theseus::pack::install_from::get_instance_from_pack(&context, location)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_create_instance(
|
||||
request: InstallCreateInstanceRequest,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallJobSnapshot> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::install::create_instance(
|
||||
&context,
|
||||
request.name.trim().to_string(),
|
||||
request.game_version,
|
||||
request.loader,
|
||||
@@ -93,8 +101,11 @@ pub async fn install_create_instance(
|
||||
pub async fn install_create_modpack_instance(
|
||||
location: CreatePackLocation,
|
||||
post_install_edit: Option<InstallPostInstallEditRequest>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallJobSnapshot> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::install::create_modpack_instance(
|
||||
&context,
|
||||
location,
|
||||
post_install_edit.map(|edit| edit.into_core()).transpose()?,
|
||||
)
|
||||
@@ -106,8 +117,11 @@ pub async fn install_import_instance(
|
||||
launcher_type: ImportLauncherType,
|
||||
base_path: PathBuf,
|
||||
instance_folder: String,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallJobSnapshot> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::install::import_instance(
|
||||
&context,
|
||||
launcher_type,
|
||||
base_path,
|
||||
instance_folder,
|
||||
@@ -118,16 +132,28 @@ pub async fn install_import_instance(
|
||||
#[tauri::command]
|
||||
pub async fn install_duplicate_instance(
|
||||
source_instance_id: String,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallJobSnapshot> {
|
||||
Ok(theseus::install::duplicate_instance(source_instance_id).await?)
|
||||
let context = invocation_context;
|
||||
Ok(
|
||||
theseus::install::duplicate_instance(&context, source_instance_id)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_existing_instance(
|
||||
instance_id: String,
|
||||
force: bool,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallJobSnapshot> {
|
||||
Ok(theseus::install::install_existing_instance(instance_id, force).await?)
|
||||
let context = invocation_context;
|
||||
Ok(theseus::install::install_existing_instance(
|
||||
&context,
|
||||
instance_id,
|
||||
force,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -135,8 +161,11 @@ pub async fn install_pack_to_existing_instance(
|
||||
instance_id: String,
|
||||
location: CreatePackLocation,
|
||||
post_install_edit: Option<InstallPostInstallEditRequest>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallJobSnapshot> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::install::install_pack_to_existing_instance(
|
||||
&context,
|
||||
instance_id,
|
||||
location,
|
||||
post_install_edit.map(|edit| edit.into_core()).transpose()?,
|
||||
@@ -157,8 +186,12 @@ pub async fn install_job_get(job_id: Uuid) -> Result<InstallJobSnapshot> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_job_retry(job_id: Uuid) -> Result<InstallJobSnapshot> {
|
||||
Ok(theseus::install::retry_job(job_id).await?)
|
||||
pub async fn install_job_retry(
|
||||
job_id: Uuid,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<InstallJobSnapshot> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::install::retry_job(&context, job_id).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -437,15 +437,25 @@ pub async fn instance_list() -> Result<Vec<Instance>> {
|
||||
pub async fn instance_get_projects(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<DashMap<String, ContentFile>> {
|
||||
Ok(theseus::instance::get_projects(instance_id, cache_behaviour).await?)
|
||||
let context = invocation_context;
|
||||
Ok(
|
||||
theseus::instance::get_projects(&context, instance_id, cache_behaviour)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn instance_get_installed_project_ids(
|
||||
instance_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<String>> {
|
||||
Ok(theseus::instance::get_installed_project_ids(instance_id).await?)
|
||||
let context = invocation_context;
|
||||
Ok(
|
||||
theseus::instance::get_installed_project_ids(&context, instance_id)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -466,27 +476,36 @@ pub async fn instance_get_install_candidates(
|
||||
pub async fn instance_content(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<ContentItem>> {
|
||||
instance_get_content_items(instance_id, cache_behaviour).await
|
||||
instance_get_content_items(instance_id, cache_behaviour, invocation_context)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn instance_get_content_items(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<ContentItem>> {
|
||||
Ok(
|
||||
theseus::instance::get_content_items(instance_id, cache_behaviour)
|
||||
.await?,
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::get_content_items(
|
||||
&context,
|
||||
instance_id,
|
||||
cache_behaviour,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn instance_get_dependencies_as_content_items(
|
||||
dependencies: Vec<Dependency>,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<ContentItem>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::get_dependencies_as_content_items(
|
||||
&context,
|
||||
dependencies,
|
||||
cache_behaviour,
|
||||
)
|
||||
@@ -497,22 +516,26 @@ pub async fn instance_get_dependencies_as_content_items(
|
||||
pub async fn instance_get_linked_modpack_info(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Option<LinkedModpackInfo>> {
|
||||
Ok(
|
||||
theseus::instance::get_linked_modpack_info(
|
||||
instance_id,
|
||||
cache_behaviour,
|
||||
)
|
||||
.await?,
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::get_linked_modpack_info(
|
||||
&context,
|
||||
instance_id,
|
||||
cache_behaviour,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn instance_get_linked_modpack_content(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<ContentItem>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::get_linked_modpack_content(
|
||||
&context,
|
||||
instance_id,
|
||||
cache_behaviour,
|
||||
)
|
||||
@@ -535,19 +558,23 @@ pub async fn instance_get_mod_full_path(
|
||||
#[tauri::command]
|
||||
pub async fn instance_get_optimal_jre_key(
|
||||
instance_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Option<JavaVersion>> {
|
||||
Ok(theseus::instance::get_optimal_jre_key(instance_id).await?)
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::get_optimal_jre_key(&context, instance_id).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn instance_check_installed(
|
||||
instance_id: &str,
|
||||
project_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<bool> {
|
||||
let context = invocation_context;
|
||||
let check_project_id = project_id;
|
||||
|
||||
if let Ok(projects) =
|
||||
theseus::instance::get_projects(instance_id, None).await
|
||||
theseus::instance::get_projects(&context, instance_id, None).await
|
||||
{
|
||||
Ok(projects.into_iter().any(|(_, project)| {
|
||||
project
|
||||
@@ -563,19 +590,26 @@ pub async fn instance_check_installed(
|
||||
#[tauri::command]
|
||||
pub async fn instance_update_all(
|
||||
instance_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<HashMap<String, String>> {
|
||||
Ok(theseus::instance::update_all_projects(instance_id).await?)
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::update_all_projects(&context, instance_id).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn instance_update_project(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<String> {
|
||||
Ok(
|
||||
theseus::instance::update_project(instance_id, project_path, None)
|
||||
.await?,
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::update_project(
|
||||
&context,
|
||||
instance_id,
|
||||
project_path,
|
||||
None,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -584,8 +618,11 @@ pub async fn instance_add_project_from_version(
|
||||
version_id: &str,
|
||||
reason: DownloadReason,
|
||||
dependent_on_version_id: Option<String>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<String> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::add_project_from_version(
|
||||
&context,
|
||||
instance_id,
|
||||
version_id,
|
||||
reason,
|
||||
@@ -598,8 +635,11 @@ pub async fn instance_add_project_from_version(
|
||||
pub async fn instance_install_project_with_dependencies(
|
||||
instance_id: &str,
|
||||
request: InstallProjectWithDependenciesRequest,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<ResolveContentPlan> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::install_project_with_dependencies(
|
||||
&context,
|
||||
instance_id,
|
||||
request,
|
||||
)
|
||||
@@ -611,8 +651,11 @@ pub async fn instance_switch_project_version_with_dependencies(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
version_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<String> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::switch_project_version_with_dependencies(
|
||||
&context,
|
||||
instance_id,
|
||||
project_path,
|
||||
version_id,
|
||||
@@ -635,8 +678,12 @@ pub async fn instance_add_project_from_path(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn instance_is_file_on_modrinth(project_path: &Path) -> Result<bool> {
|
||||
Ok(theseus::instance::is_file_on_modrinth(project_path).await?)
|
||||
pub async fn instance_is_file_on_modrinth(
|
||||
project_path: &Path,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<bool> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::is_file_on_modrinth(&context, project_path).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -666,8 +713,11 @@ pub async fn instance_remove_project(
|
||||
pub async fn instance_update_managed_modrinth_version(
|
||||
instance_id: String,
|
||||
version_id: String,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<theseus::install::InstallJobSnapshot> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::instance::update_managed_modrinth_version(
|
||||
&context,
|
||||
&instance_id,
|
||||
&version_id,
|
||||
)
|
||||
@@ -677,8 +727,13 @@ pub async fn instance_update_managed_modrinth_version(
|
||||
#[tauri::command]
|
||||
pub async fn instance_repair_managed_modrinth(
|
||||
instance_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<theseus::install::InstallJobSnapshot> {
|
||||
Ok(theseus::instance::repair_managed_modrinth(instance_id).await?)
|
||||
let context = invocation_context;
|
||||
Ok(
|
||||
theseus::instance::repair_managed_modrinth(&context, instance_id)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -689,8 +744,11 @@ pub async fn instance_export_mrpack(
|
||||
version_id: Option<String>,
|
||||
description: Option<String>,
|
||||
name: Option<String>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<()> {
|
||||
let context = invocation_context;
|
||||
theseus::instance::export_mrpack(
|
||||
&context,
|
||||
instance_id,
|
||||
export_location,
|
||||
included_overrides,
|
||||
@@ -713,12 +771,14 @@ pub async fn instance_get_pack_export_candidates(
|
||||
pub async fn instance_run(
|
||||
instance_id: &str,
|
||||
server_address: Option<String>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<ProcessMetadata> {
|
||||
let context = invocation_context;
|
||||
let quick_play = match server_address {
|
||||
Some(addr) => QuickPlayType::Server(ServerAddress::Unresolved(addr)),
|
||||
None => QuickPlayType::None,
|
||||
};
|
||||
Ok(theseus::instance::run(instance_id, quick_play).await?)
|
||||
Ok(theseus::instance::run(&context, instance_id, quick_play).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -53,8 +53,12 @@ pub async fn jre_test_jre(path: PathBuf, major_version: u32) -> Result<bool> {
|
||||
|
||||
// Auto installs java for the given java version
|
||||
#[tauri::command]
|
||||
pub async fn jre_auto_install_java(java_version: u32) -> Result<PathBuf> {
|
||||
Ok(jre::auto_install_java(java_version).await?)
|
||||
pub async fn jre_auto_install_java(
|
||||
java_version: u32,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<PathBuf> {
|
||||
let context = invocation_context;
|
||||
Ok(jre::auto_install_java(&context, java_version).await?)
|
||||
}
|
||||
|
||||
// Gets the maximum memory a system has available.
|
||||
|
||||
@@ -13,12 +13,19 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
|
||||
/// Gets the game versions from daedalus
|
||||
#[tauri::command]
|
||||
pub async fn metadata_get_game_versions() -> Result<VersionManifest> {
|
||||
Ok(theseus::metadata::get_minecraft_versions().await?)
|
||||
pub async fn metadata_get_game_versions(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<VersionManifest> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::metadata::get_minecraft_versions(&context).await?)
|
||||
}
|
||||
|
||||
/// Gets the fabric versions from daedalus
|
||||
#[tauri::command]
|
||||
pub async fn metadata_get_loader_versions(loader: &str) -> Result<Manifest> {
|
||||
Ok(theseus::metadata::get_loader_versions(loader).await?)
|
||||
pub async fn metadata_get_loader_versions(
|
||||
loader: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Manifest> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::metadata::get_loader_versions(&context, loader).await?)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||
#[tauri::command]
|
||||
pub async fn modrinth_login<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<ModrinthCredentials> {
|
||||
let context = invocation_context;
|
||||
let (auth_code_recv_socket_tx, auth_code_recv_socket) = oneshot::channel();
|
||||
let auth_code = tokio::spawn(oauth_utils::auth_code_reply::listen(
|
||||
auth_code_recv_socket_tx,
|
||||
@@ -58,7 +60,8 @@ pub async fn modrinth_login<R: Runtime>(
|
||||
));
|
||||
};
|
||||
|
||||
let credentials = mr_auth::authenticate_finish_flow(&auth_code).await?;
|
||||
let credentials =
|
||||
mr_auth::authenticate_finish_flow(&context, &auth_code).await?;
|
||||
|
||||
if let Some(main_window) = app.get_window("main") {
|
||||
main_window.set_focus().ok();
|
||||
@@ -73,8 +76,11 @@ pub async fn logout() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get() -> Result<Option<ModrinthCredentials>> {
|
||||
Ok(theseus::mr_auth::get_credentials().await?)
|
||||
pub async fn get(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Option<ModrinthCredentials>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::mr_auth::get_credentials(&context).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
+25
-10
@@ -15,30 +15,45 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
|
||||
/// Gets cached category tags from the database
|
||||
#[tauri::command]
|
||||
pub async fn tags_get_categories() -> Result<Vec<Category>> {
|
||||
Ok(theseus::tags::get_category_tags().await?)
|
||||
pub async fn tags_get_categories(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<Category>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::tags::get_category_tags(&context).await?)
|
||||
}
|
||||
|
||||
/// Gets cached report type tags from the database
|
||||
#[tauri::command]
|
||||
pub async fn tags_get_report_types() -> Result<Vec<String>> {
|
||||
Ok(theseus::tags::get_report_type_tags().await?)
|
||||
pub async fn tags_get_report_types(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<String>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::tags::get_report_type_tags(&context).await?)
|
||||
}
|
||||
|
||||
/// Gets cached loader tags from the database
|
||||
#[tauri::command]
|
||||
pub async fn tags_get_loaders() -> Result<Vec<Loader>> {
|
||||
Ok(theseus::tags::get_loader_tags().await?)
|
||||
pub async fn tags_get_loaders(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<Loader>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::tags::get_loader_tags(&context).await?)
|
||||
}
|
||||
|
||||
/// Gets cached game version tags from the database
|
||||
#[tauri::command]
|
||||
pub async fn tags_get_game_versions() -> Result<Vec<GameVersion>> {
|
||||
Ok(theseus::tags::get_game_version_tags().await?)
|
||||
pub async fn tags_get_game_versions(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<GameVersion>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::tags::get_game_version_tags(&context).await?)
|
||||
}
|
||||
|
||||
/// Gets cached donation platform tags from the database
|
||||
#[tauri::command]
|
||||
pub async fn tags_get_donation_platforms() -> Result<Vec<DonationPlatform>> {
|
||||
Ok(theseus::tags::get_donation_platform_tags().await?)
|
||||
pub async fn tags_get_donation_platforms(
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Vec<DonationPlatform>> {
|
||||
let context = invocation_context;
|
||||
Ok(theseus::tags::get_donation_platform_tags(&context).await?)
|
||||
}
|
||||
|
||||
@@ -196,8 +196,10 @@ pub async fn remove_server_from_instance(
|
||||
#[tauri::command]
|
||||
pub async fn get_instance_protocol_version(
|
||||
instance_id: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<Option<ProtocolVersion>> {
|
||||
Ok(worlds::get_instance_protocol_version(instance_id).await?)
|
||||
let context = invocation_context;
|
||||
Ok(worlds::get_instance_protocol_version(&context, instance_id).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -212,9 +214,15 @@ pub async fn get_server_status(
|
||||
pub async fn start_join_singleplayer_world(
|
||||
instance_id: &str,
|
||||
world: String,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<ProcessMetadata> {
|
||||
let process =
|
||||
instance::run(instance_id, QuickPlayType::Singleplayer(world)).await?;
|
||||
let context = invocation_context;
|
||||
let process = instance::run(
|
||||
&context,
|
||||
instance_id,
|
||||
QuickPlayType::Singleplayer(world),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(process)
|
||||
}
|
||||
@@ -223,8 +231,11 @@ pub async fn start_join_singleplayer_world(
|
||||
pub async fn start_join_server(
|
||||
instance_id: &str,
|
||||
address: &str,
|
||||
invocation_context: theseus::InvocationContext,
|
||||
) -> Result<ProcessMetadata> {
|
||||
let context = invocation_context;
|
||||
let process = instance::run(
|
||||
&context,
|
||||
instance_id,
|
||||
QuickPlayType::Server(ServerAddress::Unresolved(address.to_owned())),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<LoadingBar />
|
||||
</ClientOnly>
|
||||
<NotificationPanel />
|
||||
<AdsConsentNotification />
|
||||
<I18nDebugPanel />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
@@ -12,6 +13,7 @@
|
||||
<script setup lang="ts">
|
||||
import { I18nDebugPanel, LoadingBar, NotificationPanel } from '@modrinth/ui'
|
||||
|
||||
import AdsConsentNotification from '~/components/ui/AdsConsentNotification.vue'
|
||||
import { setupProviders } from '~/providers/setup.ts'
|
||||
|
||||
import { useAuth } from './composables/auth'
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
type WebNotification,
|
||||
} from '@modrinth/ui'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
type ConsentAction = 'accept' | 'reject' | 'manage'
|
||||
|
||||
interface TcfData {
|
||||
eventStatus?: string
|
||||
listenerId?: number
|
||||
}
|
||||
|
||||
type TcfCallback = (data: TcfData, success: boolean) => void
|
||||
type TcfApi = (command: string, version: number, callback: TcfCallback, parameter?: unknown) => void
|
||||
|
||||
const CMP_HIDDEN_CLASS = 'modrinth-cmp-summary-hidden'
|
||||
const notificationManager = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'ads-consent.title',
|
||||
defaultMessage: 'Your privacy and how ads support Modrinth',
|
||||
},
|
||||
body: {
|
||||
id: 'ads-consent.body',
|
||||
defaultMessage:
|
||||
'Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies on the website to personalize ads and measure performance.',
|
||||
},
|
||||
manage: {
|
||||
id: 'ads-consent.manage',
|
||||
defaultMessage: 'Manage preferences',
|
||||
},
|
||||
reject: {
|
||||
id: 'ads-consent.reject',
|
||||
defaultMessage: 'Reject all',
|
||||
},
|
||||
accept: {
|
||||
id: 'ads-consent.accept',
|
||||
defaultMessage: 'Accept all',
|
||||
},
|
||||
})
|
||||
|
||||
let notificationId: WebNotification['id'] | null = null
|
||||
let tcfListenerId: number | undefined
|
||||
let listenerInstalled = false
|
||||
let managingPreferences = false
|
||||
let consentContainerObserver: MutationObserver | undefined
|
||||
const consentContainerContains = new Map<HTMLElement, HTMLElement['contains']>()
|
||||
|
||||
function getTcfApi(): TcfApi | undefined {
|
||||
return (window as typeof window & { __tcfapi?: TcfApi }).__tcfapi
|
||||
}
|
||||
|
||||
function setConsentUiHidden(hidden: boolean) {
|
||||
document.documentElement.classList.toggle(CMP_HIDDEN_CLASS, hidden)
|
||||
patchConsentFocusTrapContainer()
|
||||
}
|
||||
|
||||
function patchConsentFocusTrapContainer() {
|
||||
const container = document.querySelector<HTMLElement>('#qc-cmp2-ui')
|
||||
if (!container || consentContainerContains.has(container)) return
|
||||
|
||||
const originalContains = container.contains
|
||||
// InMobi's focus trap otherwise cancels clicks outside its hidden container.
|
||||
container.contains = (node: Node | null) =>
|
||||
document.documentElement.classList.contains(CMP_HIDDEN_CLASS) ||
|
||||
originalContains.call(container, node)
|
||||
consentContainerContains.set(container, originalContains)
|
||||
}
|
||||
|
||||
function getConsentContainers(): ParentNode[] {
|
||||
const containers = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('#qc-cmp2-container, #qc-cmp2-main, #qc-cmp2-ui'),
|
||||
)
|
||||
|
||||
return containers.length > 0 ? containers : [document]
|
||||
}
|
||||
|
||||
function matchesButtonText(button: HTMLButtonElement, terms: string[]): boolean {
|
||||
const text = [button.textContent, button.getAttribute('aria-label')]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
return terms.some((term) => text.includes(term))
|
||||
}
|
||||
|
||||
function findConsentButton(action: ConsentAction): HTMLButtonElement | null {
|
||||
const containers = getConsentContainers()
|
||||
const summaryButtons = containers.flatMap((container) =>
|
||||
Array.from(container.querySelectorAll<HTMLButtonElement>('.qc-cmp2-summary-buttons button')),
|
||||
)
|
||||
const queryButton = (selector: string) =>
|
||||
containers
|
||||
.map((container) => container.querySelector<HTMLButtonElement>(selector))
|
||||
.find((button) => button && !button.disabled) ?? null
|
||||
|
||||
if (action === 'accept') {
|
||||
const explicitAcceptButton = queryButton(
|
||||
'[data-testid="accept-all"], [data-testid="agree-button"], #accept-btn',
|
||||
)
|
||||
if (explicitAcceptButton) return explicitAcceptButton
|
||||
|
||||
const textMatch = summaryButtons.find((button) =>
|
||||
matchesButtonText(button, ['accept all', 'agree to all', 'allow all']),
|
||||
)
|
||||
if (textMatch) return textMatch
|
||||
if (summaryButtons.length >= 3 && !summaryButtons[2].disabled) return summaryButtons[2]
|
||||
|
||||
return queryButton('.qc-cmp2-summary-buttons button[mode="primary"]')
|
||||
}
|
||||
|
||||
if (action === 'reject') {
|
||||
const explicitRejectButton = queryButton(
|
||||
'[data-testid="reject-all"], [data-testid="disagree-button"], #disagree-btn, #reject-btn',
|
||||
)
|
||||
if (explicitRejectButton) return explicitRejectButton
|
||||
|
||||
const textMatch = summaryButtons.find((button) =>
|
||||
matchesButtonText(button, ['reject all', 'disagree', 'deny all']),
|
||||
)
|
||||
if (textMatch) return textMatch
|
||||
if (summaryButtons.length >= 3 && !summaryButtons[1].disabled) return summaryButtons[1]
|
||||
|
||||
const secondaryButtons = containers.flatMap((container) =>
|
||||
Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>(
|
||||
'.qc-cmp2-summary-buttons button[mode="secondary"]',
|
||||
),
|
||||
),
|
||||
)
|
||||
if (secondaryButtons.length > 1 && !secondaryButtons[1].disabled) return secondaryButtons[1]
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
queryButton(
|
||||
'[data-testid="manage-preferences"], [data-testid="show-options"], .qc-cmp2-summary-buttons > button[mode="secondary"]:first-of-type',
|
||||
) ??
|
||||
summaryButtons.find((button) =>
|
||||
matchesButtonText(button, ['manage', 'preference', 'settings', 'options']),
|
||||
) ??
|
||||
summaryButtons.find((button) => !button.disabled) ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
function clickConsentButtonWhenReady(action: ConsentAction, timeoutMs: number): Promise<boolean> {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
|
||||
return new Promise((resolve) => {
|
||||
function tryClick() {
|
||||
const button = findConsentButton(action)
|
||||
if (button) {
|
||||
button.click()
|
||||
resolve(true)
|
||||
} else if (Date.now() >= deadline) {
|
||||
resolve(false)
|
||||
} else {
|
||||
setTimeout(tryClick, 50)
|
||||
}
|
||||
}
|
||||
|
||||
tryClick()
|
||||
})
|
||||
}
|
||||
|
||||
async function performConsentAction(action: ConsentAction) {
|
||||
if (action === 'manage') {
|
||||
managingPreferences = true
|
||||
setConsentUiHidden(false)
|
||||
}
|
||||
|
||||
const clicked = await clickConsentButtonWhenReady(action, action === 'manage' ? 2500 : 1000)
|
||||
|
||||
if (clicked) return
|
||||
|
||||
managingPreferences = action === 'manage'
|
||||
setConsentUiHidden(false)
|
||||
|
||||
if (action === 'manage') {
|
||||
const tcfApi = getTcfApi()
|
||||
if (tcfApi) {
|
||||
tcfApi('displayConsentUi', 2, () => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showConsentNotification() {
|
||||
setConsentUiHidden(true)
|
||||
|
||||
if (
|
||||
notificationId !== null &&
|
||||
notificationManager
|
||||
.getNotifications()
|
||||
.some((notification) => notification.id === notificationId)
|
||||
)
|
||||
return
|
||||
|
||||
const notification = notificationManager.addNotification({
|
||||
title: formatMessage(messages.title),
|
||||
text: formatMessage(messages.body),
|
||||
type: 'neutral',
|
||||
autoCloseMs: null,
|
||||
dismissible: false,
|
||||
copyable: false,
|
||||
noIcon: true,
|
||||
containerClass: 'py-2 !rounded-2xl',
|
||||
buttons: [
|
||||
{
|
||||
label: formatMessage(messages.manage),
|
||||
action: () => performConsentAction('manage'),
|
||||
keepOpen: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.reject),
|
||||
action: () => performConsentAction('reject'),
|
||||
color: 'brand',
|
||||
keepOpen: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.accept),
|
||||
action: () => performConsentAction('accept'),
|
||||
color: 'brand',
|
||||
keepOpen: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
notificationId = notification.id
|
||||
}
|
||||
|
||||
function finishConsent() {
|
||||
managingPreferences = false
|
||||
setConsentUiHidden(false)
|
||||
|
||||
if (notificationId !== null) {
|
||||
notificationManager.removeNotification(notificationId)
|
||||
notificationId = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleTcfConsentEvent(data: TcfData, success: boolean) {
|
||||
if (!success) return
|
||||
|
||||
if (data.listenerId !== undefined) {
|
||||
tcfListenerId = data.listenerId
|
||||
}
|
||||
|
||||
if (data.eventStatus === 'cmpuishown') {
|
||||
if (!managingPreferences) {
|
||||
showConsentNotification()
|
||||
}
|
||||
} else if (data.eventStatus === 'useractioncomplete') {
|
||||
finishConsent()
|
||||
}
|
||||
}
|
||||
|
||||
function installTcfConsentListener() {
|
||||
if (listenerInstalled) return
|
||||
|
||||
const tcfApi = getTcfApi()
|
||||
if (!tcfApi) return
|
||||
|
||||
listenerInstalled = true
|
||||
tcfApi('addEventListener', 2, handleTcfConsentEvent)
|
||||
}
|
||||
|
||||
function handleDocumentClick(event: MouseEvent) {
|
||||
if (!managingPreferences || !(event.target instanceof Element)) return
|
||||
if (!event.target.closest('.qc-cmp2-close-icon')) return
|
||||
|
||||
setTimeout(() => {
|
||||
if (notificationId === null) return
|
||||
|
||||
managingPreferences = false
|
||||
setConsentUiHidden(true)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
consentContainerObserver = new MutationObserver(patchConsentFocusTrapContainer)
|
||||
consentContainerObserver.observe(document.body, { childList: true, subtree: true })
|
||||
patchConsentFocusTrapContainer()
|
||||
installTcfConsentListener()
|
||||
window.addEventListener('modrinth-cmp-ready', installTcfConsentListener)
|
||||
document.addEventListener('click', handleDocumentClick, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
consentContainerObserver?.disconnect()
|
||||
window.removeEventListener('modrinth-cmp-ready', installTcfConsentListener)
|
||||
document.removeEventListener('click', handleDocumentClick, true)
|
||||
setConsentUiHidden(false)
|
||||
for (const [container, originalContains] of consentContainerContains) {
|
||||
container.contains = originalContains
|
||||
}
|
||||
consentContainerContains.clear()
|
||||
|
||||
const tcfApi = getTcfApi()
|
||||
if (tcfApi && tcfListenerId !== undefined) {
|
||||
tcfApi('removeEventListener', 2, () => {}, tcfListenerId)
|
||||
}
|
||||
|
||||
if (notificationId !== null) {
|
||||
notificationManager.removeNotification(notificationId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html.modrinth-cmp-summary-hidden .qc-cmp2-container,
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-container,
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-main,
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-ui {
|
||||
display: none !important;
|
||||
z-index: -1 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<nav :aria-label="ariaLabel" class="w-full">
|
||||
<ul
|
||||
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4"
|
||||
class="card-shadow m-0 mb-3 flex list-none flex-col items-start gap-1.5 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4"
|
||||
:class="{ 'pt-3': filteredItems?.[0]?.type === 'heading' }"
|
||||
>
|
||||
<slot v-if="hasSlotContent" />
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<PageHeader :title="organization.name" :summary="organization.description">
|
||||
<template #leading>
|
||||
<Avatar
|
||||
:src="organization.icon_url"
|
||||
:alt="organization.name"
|
||||
:tint-by="organization.id"
|
||||
size="96px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #badges>
|
||||
<PageHeaderBadgeItem :icon="OrganizationIcon" class="px-0 text-primary">
|
||||
{{ formatMessage(messages.organizationLabel) }}
|
||||
</PageHeaderBadgeItem>
|
||||
</template>
|
||||
|
||||
<template #metadata>
|
||||
<PageHeaderMetadata>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="UsersIcon"
|
||||
:value="membersCount"
|
||||
:label="formatMessage(messages.membersLabel)"
|
||||
/>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="BoxIcon"
|
||||
:value="projectsCount"
|
||||
:label="formatMessage(messages.projectsLabel)"
|
||||
/>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="DownloadIcon"
|
||||
:value="downloads"
|
||||
:label="formatMessage(messages.downloadsLabel)"
|
||||
:tooltip="formatNumber(downloads)"
|
||||
/>
|
||||
</PageHeaderMetadata>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<ButtonStyled v-if="canManage" size="large">
|
||||
<nuxt-link :to="`/organization/${organization.slug}/settings`">
|
||||
<SettingsIcon />
|
||||
{{ formatMessage(messages.manage) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="moreActions"
|
||||
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
||||
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</PageHeaderActions>
|
||||
</template>
|
||||
</PageHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BoxIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
MoreVerticalIcon,
|
||||
OrganizationIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
PageHeader,
|
||||
PageHeaderActions,
|
||||
PageHeaderBadgeItem,
|
||||
PageHeaderMetadata,
|
||||
PageHeaderMetadataNumberItem,
|
||||
TeleportOverflowMenu,
|
||||
type TeleportOverflowMenuItem,
|
||||
useFormatNumber,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const messages = defineMessages({
|
||||
downloadsLabel: {
|
||||
id: 'organization.label.downloads',
|
||||
defaultMessage: 'downloads',
|
||||
},
|
||||
manage: {
|
||||
id: 'organization.button.manage',
|
||||
defaultMessage: 'Manage',
|
||||
},
|
||||
manageProjects: {
|
||||
id: 'organization.button.manage-projects',
|
||||
defaultMessage: 'Manage projects',
|
||||
},
|
||||
membersLabel: {
|
||||
id: 'organization.label.members',
|
||||
defaultMessage: 'members',
|
||||
},
|
||||
organizationLabel: {
|
||||
id: 'organization.label.organization',
|
||||
defaultMessage: 'Organization',
|
||||
},
|
||||
projectsLabel: {
|
||||
id: 'organization.label.projects',
|
||||
defaultMessage: 'projects',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
organization: {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
description?: string | null
|
||||
icon_url?: string | null
|
||||
}
|
||||
membersCount: number
|
||||
projectsCount: number
|
||||
downloads: number
|
||||
canManage?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
manageProjects: []
|
||||
copyId: []
|
||||
copyPermalink: []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatNumber = useFormatNumber()
|
||||
|
||||
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
|
||||
{
|
||||
id: 'manage-projects',
|
||||
label: formatMessage(messages.manageProjects),
|
||||
icon: BoxIcon,
|
||||
action: () => emit('manageProjects'),
|
||||
shown: props.canManage,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: props.canManage,
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyId'),
|
||||
},
|
||||
{
|
||||
id: 'copy-permalink',
|
||||
label: formatMessage(commonMessages.copyPermalinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyPermalink'),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -1,111 +1,83 @@
|
||||
<template>
|
||||
<div>
|
||||
<Modal ref="modalOpen" header="Transfer Projects">
|
||||
<div class="universal-modal items">
|
||||
<div class="table">
|
||||
<div class="table-head table-row">
|
||||
<div class="check-cell table-cell">
|
||||
<Checkbox
|
||||
:model-value="selectedProjects.length === props.projects.length"
|
||||
@update:model-value="toggleSelectedProjects()"
|
||||
/>
|
||||
<NewModal ref="modalOpen" header="Transfer projects" no-padding width="60rem">
|
||||
<div class="max-h-[70vh] overflow-y-auto">
|
||||
<Table
|
||||
class="!rounded-none !border-0"
|
||||
:columns="projectTableColumns"
|
||||
:data="props.projects"
|
||||
row-key="id"
|
||||
table-min-width="42rem"
|
||||
table-layout="auto"
|
||||
>
|
||||
<template #empty-state>
|
||||
<div class="flex h-48 items-center justify-center text-secondary">
|
||||
No projects available to transfer.
|
||||
</div>
|
||||
<div class="table-cell">Icon</div>
|
||||
<div class="table-cell">Name</div>
|
||||
<div class="table-cell">ID</div>
|
||||
<div class="table-cell">Type</div>
|
||||
<div class="table-cell" />
|
||||
</div>
|
||||
<div v-for="project in props.projects" :key="`project-${project.id}`" class="table-row">
|
||||
<div class="check-cell table-cell">
|
||||
<Checkbox
|
||||
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||
:model-value="selectedProjects.includes(project)"
|
||||
@update:model-value="
|
||||
selectedProjects.includes(project)
|
||||
? (selectedProjects = selectedProjects.filter((it) => it !== project))
|
||||
: selectedProjects.push(project)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-cell">
|
||||
<nuxt-link tabindex="-1" :to="`/project/${project.slug ? project.slug : project.id}`">
|
||||
<Avatar
|
||||
</template>
|
||||
<template #header-select>
|
||||
<Checkbox
|
||||
class="h-full w-full justify-center"
|
||||
:model-value="allTransferableProjectsSelected"
|
||||
@update:model-value="toggleAllTransferableProjects()"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-select="{ row: project }">
|
||||
<Checkbox
|
||||
class="h-full w-full justify-center"
|
||||
:disabled="isProjectTransferDisabled(project)"
|
||||
:model-value="isProjectSelected(project)"
|
||||
@update:model-value="toggleProjectSelection(project)"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-name="{ row: project }">
|
||||
<nuxt-link class="project-name-cell" :to="getProjectUrl(project)">
|
||||
<span
|
||||
class="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded text-primary"
|
||||
>
|
||||
<img
|
||||
v-if="project.icon_url"
|
||||
:src="project.icon_url"
|
||||
aria-hidden="true"
|
||||
:alt="'Icon for ' + project.name"
|
||||
no-shadow
|
||||
:alt="'Icon for ' + project.title"
|
||||
class="h-full w-full rounded object-cover"
|
||||
/>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<span class="project-title">
|
||||
<nuxt-link
|
||||
class="hover-link wrap-as-needed"
|
||||
:to="`/project/${project.slug ? project.slug : project.id}`"
|
||||
>
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<BoxIcon v-else class="h-full w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<span class="wrap-as-needed">{{ project.title }}</span>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template #cell-id="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
<CopyCode :text="project.id" />
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<BoxIcon />
|
||||
<span>{{
|
||||
formatProjectType(
|
||||
$getProjectTypeForDisplay(
|
||||
project.project_types?.[0] ?? 'project',
|
||||
project.loaders,
|
||||
),
|
||||
)
|
||||
}}</span>
|
||||
</template>
|
||||
<template #cell-type="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
{{ getProjectDisplayType(project) }}
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link :to="`/project/${project.slug ? project.slug : project.id}/settings`">
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="push-right input-group">
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="$refs.modalOpen?.hide()">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!selectedProjects?.length" @click="onSubmitHandler()">
|
||||
<button :disabled="selectedProjects.length === 0" @click="submitTransfer()">
|
||||
<TransferIcon />
|
||||
<span>
|
||||
Transfer
|
||||
<span>
|
||||
{{
|
||||
selectedProjects.length === props.projects.length
|
||||
? 'All'
|
||||
: selectedProjects.length
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ ' ' }}
|
||||
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||
</span>
|
||||
</span>
|
||||
Transfer {{ selectedProjects.length }}
|
||||
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
</NewModal>
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.modalOpen?.show()">
|
||||
<button @click="show($event)">
|
||||
<TransferIcon />
|
||||
<span>Transfer projects</span>
|
||||
</button>
|
||||
@@ -114,12 +86,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, Checkbox, CopyCode, Modal } from '@modrinth/ui'
|
||||
import { BoxIcon, TransferIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
CopyCode,
|
||||
NewModal,
|
||||
Table,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
import { getProjectTypeForUrl } from '~/helpers/projects.js'
|
||||
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
const modalOpen = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
projects: {
|
||||
@@ -128,126 +109,113 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
// define emit for submission
|
||||
const emit = defineEmits(['submit'])
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const selectedProjects = ref([])
|
||||
const modalOpen = ref(null)
|
||||
const selectedProjectIds = ref([])
|
||||
|
||||
const toggleSelectedProjects = () => {
|
||||
if (selectedProjects.value.length === props.projects.length) {
|
||||
selectedProjects.value = []
|
||||
} else {
|
||||
selectedProjects.value = props.projects
|
||||
}
|
||||
const projectTableColumns = [
|
||||
{
|
||||
key: 'select',
|
||||
width: '3rem',
|
||||
headerClass: '!p-0',
|
||||
cellClass: '!overflow-visible !p-0',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
width: '22rem',
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
label: 'ID',
|
||||
width: '13rem',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: 'Type',
|
||||
width: '10rem',
|
||||
},
|
||||
]
|
||||
|
||||
const transferableProjects = computed(() =>
|
||||
props.projects.filter((project) => !isProjectTransferDisabled(project)),
|
||||
)
|
||||
const selectedProjects = computed(() =>
|
||||
props.projects.filter((project) => selectedProjectIds.value.includes(project.id)),
|
||||
)
|
||||
const allTransferableProjectsSelected = computed(
|
||||
() =>
|
||||
transferableProjects.value.length > 0 &&
|
||||
transferableProjects.value.every((project) => selectedProjectIds.value.includes(project.id)),
|
||||
)
|
||||
|
||||
function isProjectTransferDisabled(project) {
|
||||
return (project.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||
}
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
if (selectedProjects.value.length === 0) {
|
||||
function isProjectSelected(project) {
|
||||
return selectedProjectIds.value.includes(project.id)
|
||||
}
|
||||
|
||||
function toggleProjectSelection(project) {
|
||||
if (isProjectTransferDisabled(project)) return
|
||||
|
||||
if (isProjectSelected(project)) {
|
||||
selectedProjectIds.value = selectedProjectIds.value.filter((id) => id !== project.id)
|
||||
return
|
||||
}
|
||||
emit('submit', selectedProjects.value)
|
||||
selectedProjects.value = []
|
||||
|
||||
selectedProjectIds.value = [...selectedProjectIds.value, project.id]
|
||||
}
|
||||
|
||||
function toggleAllTransferableProjects() {
|
||||
selectedProjectIds.value = allTransferableProjectsSelected.value
|
||||
? []
|
||||
: transferableProjects.value.map((project) => project.id)
|
||||
}
|
||||
|
||||
function getProjectUrl(project) {
|
||||
const projectType = getProjectTypeForUrl(project.project_type, project.loaders)
|
||||
return '/' + projectType + '/' + (project.slug || project.id)
|
||||
}
|
||||
|
||||
function getProjectDisplayType(project) {
|
||||
return formatProjectType(getProjectTypeForUrl(project.project_type, project.loaders))
|
||||
}
|
||||
|
||||
function show(event) {
|
||||
modalOpen.value?.show(event)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modalOpen.value?.hide()
|
||||
}
|
||||
|
||||
function submitTransfer() {
|
||||
if (selectedProjects.value.length === 0) return
|
||||
|
||||
emit('submit', selectedProjects.value)
|
||||
selectedProjectIds.value = []
|
||||
hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table {
|
||||
display: grid;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
margin-top: var(--gap-md);
|
||||
border: 1px solid var(--color-divider);
|
||||
background-color: var(--color-raised-bg);
|
||||
|
||||
.table-row {
|
||||
grid-template-columns: 2.75rem 3.75rem 2fr 1fr 1fr 3.5rem;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-xs);
|
||||
padding: var(--gap-md);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.check-cell {
|
||||
padding-left: var(--gap-md);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name type settings' 'checkbox icon id type settings';
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(1) {
|
||||
grid-area: checkbox;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
:nth-child(3) {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
:nth-child(4) {
|
||||
grid-area: id;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(5) {
|
||||
grid-area: type;
|
||||
}
|
||||
|
||||
:nth-child(6) {
|
||||
grid-area: settings;
|
||||
}
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
|
||||
:nth-child(2),
|
||||
:nth-child(3),
|
||||
:nth-child(4),
|
||||
:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
|
||||
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(5) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
}
|
||||
}
|
||||
.project-name-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 3.5rem;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-sm);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
padding: var(--gap-md);
|
||||
.project-name-cell:hover,
|
||||
.project-name-cell:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<ButtonStyled size="large" circular>
|
||||
<PopoutMenu
|
||||
v-if="authUser"
|
||||
:tooltip="
|
||||
saved ? formatMessage(commonMessages.savedLabel) : formatMessage(commonMessages.saveButton)
|
||||
"
|
||||
from="top-right"
|
||||
:aria-label="formatMessage(commonMessages.saveButton)"
|
||||
:dropdown-id="`${baseId}-save`"
|
||||
>
|
||||
<BookmarkIcon aria-hidden="true" :fill="saved ? 'currentColor' : 'none'" />
|
||||
<template #menu>
|
||||
<StyledInput
|
||||
v-model="displayCollectionsSearch"
|
||||
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
|
||||
wrapper-class="menu-search"
|
||||
/>
|
||||
<div v-if="filteredCollections.length > 0" class="collections-list text-primary">
|
||||
<Checkbox
|
||||
v-for="option in filteredCollections"
|
||||
:key="option.id"
|
||||
:model-value="option.projects.includes(projectId)"
|
||||
class="popout-checkbox"
|
||||
@update:model-value="() => collectProject(option, projectId)"
|
||||
>
|
||||
{{ option.name }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div v-else class="menu-text">
|
||||
<p class="popout-text">{{ noCollectionsLabel }}</p>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button class="mx-3 mb-3" @click="createCollection">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ createNewCollectionLabel }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
<nuxt-link
|
||||
v-else
|
||||
v-tooltip="formatMessage(commonMessages.saveButton)"
|
||||
:to="signInRoute"
|
||||
:aria-label="formatMessage(commonMessages.saveButton)"
|
||||
>
|
||||
<BookmarkIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BookmarkIcon, PlusIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
PopoutMenu,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
type CollectionOption = {
|
||||
id: string
|
||||
name: string
|
||||
projects: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
authUser?: unknown
|
||||
signInRoute: RouteLocationRaw
|
||||
projectId: string
|
||||
collections: CollectionOption[]
|
||||
saved: boolean
|
||||
baseId: string
|
||||
noCollectionsLabel: string
|
||||
createNewCollectionLabel: string
|
||||
collectProject: (option: CollectionOption, projectId: string) => void | Promise<void>
|
||||
createCollection: (event: MouseEvent) => void
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const displayCollectionsSearch = ref('')
|
||||
|
||||
const filteredCollections = computed(() =>
|
||||
props.collections
|
||||
.filter((collection) =>
|
||||
collection.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popout-checkbox {
|
||||
padding: var(--gap-sm) var(--gap-md);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
padding: 0 var(--gap-md);
|
||||
font-size: var(--font-size-nm);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.menu-search {
|
||||
margin: var(--gap-sm) var(--gap-md);
|
||||
width: calc(100% - var(--gap-md) * 2);
|
||||
}
|
||||
|
||||
.collections-list {
|
||||
max-height: 40rem;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--radius-md);
|
||||
margin: var(--gap-sm) var(--gap-md);
|
||||
padding: var(--gap-sm);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<PageHeader :title="user.username" :summary="summary">
|
||||
<template #leading>
|
||||
<Avatar
|
||||
:src="user.avatar_url"
|
||||
:alt="user.username"
|
||||
:size="isModrinthUser ? '64px' : '96px'"
|
||||
:tint-by="user.username"
|
||||
circle
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="isOfficialAccount || showAffiliateBadge" #badges>
|
||||
<PageHeaderBadgeItem
|
||||
v-if="isOfficialAccount"
|
||||
:icon="BadgeCheckIcon"
|
||||
:icon-props="{ fill: 'var(--color-brand-highlight)' }"
|
||||
:tooltip="formatMessage(messages.officialAccount)"
|
||||
class="border-brand-highlight bg-brand-highlight text-brand"
|
||||
>
|
||||
{{ formatMessage(messages.officialAccount) }}
|
||||
</PageHeaderBadgeItem>
|
||||
<PageHeaderBadgeItem
|
||||
v-if="showAffiliateBadge"
|
||||
:icon="AffiliateIcon"
|
||||
class="border-brand-highlight bg-brand-highlight text-brand"
|
||||
>
|
||||
{{ formatMessage(messages.affiliateLabel) }}
|
||||
</PageHeaderBadgeItem>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.summary" #summary>
|
||||
<slot name="summary" />
|
||||
</template>
|
||||
|
||||
<template v-if="!isModrinthUser" #metadata>
|
||||
<PageHeaderMetadata>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="BoxIcon"
|
||||
:value="projectsCount"
|
||||
:label="formatMessage(messages.profileProjectCountLabel, { count: projectsCount })"
|
||||
/>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="DownloadIcon"
|
||||
:value="downloads"
|
||||
:label="formatMessage(messages.profileDownloadCountLabel, { count: downloads })"
|
||||
:tooltip="downloadsTooltip"
|
||||
/>
|
||||
<PageHeaderMetadataTimeItem
|
||||
:icon="CalendarIcon"
|
||||
:date="user.created"
|
||||
:label="formatMessage(messages.profileJoinedLabel)"
|
||||
:tooltip="joinedTooltip"
|
||||
/>
|
||||
</PageHeaderMetadata>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<ButtonStyled v-if="isSelf" size="large">
|
||||
<nuxt-link to="/settings/profile">
|
||||
<EditIcon />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="moreActions"
|
||||
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
||||
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</PageHeaderActions>
|
||||
</template>
|
||||
</PageHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
AffiliateIcon,
|
||||
BadgeCheckIcon,
|
||||
BoxIcon,
|
||||
CalendarIcon,
|
||||
ChartIcon,
|
||||
ClipboardCopyIcon,
|
||||
CurrencyIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
InfoIcon,
|
||||
MoreVerticalIcon,
|
||||
ReportIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
PageHeader,
|
||||
PageHeaderActions,
|
||||
PageHeaderBadgeItem,
|
||||
PageHeaderMetadata,
|
||||
PageHeaderMetadataNumberItem,
|
||||
PageHeaderMetadataTimeItem,
|
||||
TeleportOverflowMenu,
|
||||
type TeleportOverflowMenuItem,
|
||||
useFormatDateTime,
|
||||
useFormatNumber,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const messages = defineMessages({
|
||||
affiliateLabel: {
|
||||
id: 'profile.label.affiliate',
|
||||
defaultMessage: 'Affiliate',
|
||||
},
|
||||
analyticsButton: {
|
||||
id: 'profile.button.analytics',
|
||||
defaultMessage: 'View user analytics',
|
||||
},
|
||||
billingButton: {
|
||||
id: 'profile.button.billing',
|
||||
defaultMessage: 'Manage user billing',
|
||||
},
|
||||
editRoleButton: {
|
||||
id: 'profile.button.edit-role',
|
||||
defaultMessage: 'Edit role',
|
||||
},
|
||||
infoButton: {
|
||||
id: 'profile.button.info',
|
||||
defaultMessage: 'View user details',
|
||||
},
|
||||
officialAccount: {
|
||||
id: 'profile.official-account',
|
||||
defaultMessage: 'Official Modrinth account',
|
||||
},
|
||||
profileJoinedLabel: {
|
||||
id: 'profile.label.joined',
|
||||
defaultMessage: 'Joined',
|
||||
},
|
||||
profileProjectCountLabel: {
|
||||
id: 'profile.label.project-count',
|
||||
defaultMessage: '{count, plural, one {project} other {projects}}',
|
||||
},
|
||||
profileDownloadCountLabel: {
|
||||
id: 'profile.label.download-count',
|
||||
defaultMessage: '{count, plural, one {download} other {downloads}}',
|
||||
},
|
||||
profileManageProjectsButton: {
|
||||
id: 'profile.button.manage-projects',
|
||||
defaultMessage: 'Manage projects',
|
||||
},
|
||||
removeAffiliateButton: {
|
||||
id: 'profile.button.remove-affiliate',
|
||||
defaultMessage: 'Remove as affiliate',
|
||||
},
|
||||
setAffiliateButton: {
|
||||
id: 'profile.button.set-affiliate',
|
||||
defaultMessage: 'Set as affiliate',
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
user: Labrinth.Users.v3.User
|
||||
summary?: string | null
|
||||
authUser?: Labrinth.Users.v3.User | null
|
||||
isModrinthUser?: boolean
|
||||
isOfficialAccount?: boolean
|
||||
showAffiliateBadge?: boolean
|
||||
isAffiliate?: boolean
|
||||
isSelf?: boolean
|
||||
isAdmin?: boolean
|
||||
isStaff?: boolean
|
||||
projectsCount?: number
|
||||
downloads?: number
|
||||
}>(),
|
||||
{
|
||||
summary: null,
|
||||
authUser: null,
|
||||
isModrinthUser: false,
|
||||
isOfficialAccount: false,
|
||||
showAffiliateBadge: false,
|
||||
isAffiliate: false,
|
||||
isSelf: false,
|
||||
isAdmin: false,
|
||||
isStaff: false,
|
||||
projectsCount: 0,
|
||||
downloads: 0,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
manageProjects: []
|
||||
report: []
|
||||
copyId: []
|
||||
copyPermalink: []
|
||||
openBilling: []
|
||||
toggleAffiliate: []
|
||||
openInfo: []
|
||||
openAnalytics: []
|
||||
editRole: []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const formatNumber = useFormatNumber()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
const downloadsTooltip = computed(() => formatNumber(props.downloads))
|
||||
const joinedTooltip = computed(() => formatDateTime(props.user.created))
|
||||
|
||||
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
|
||||
{
|
||||
id: 'manage-projects',
|
||||
label: formatMessage(messages.profileManageProjectsButton),
|
||||
icon: BoxIcon,
|
||||
action: () => emit('manageProjects'),
|
||||
shown: props.isSelf,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: props.isSelf,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: formatMessage(commonMessages.reportButton),
|
||||
icon: ReportIcon,
|
||||
action: () => emit('report'),
|
||||
color: 'red',
|
||||
shown: props.authUser?.id !== props.user.id,
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyId'),
|
||||
},
|
||||
{
|
||||
id: 'copy-permalink',
|
||||
label: formatMessage(commonMessages.copyPermalinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyPermalink'),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: props.isAdmin,
|
||||
},
|
||||
{
|
||||
id: 'open-billing',
|
||||
label: formatMessage(messages.billingButton),
|
||||
icon: CurrencyIcon,
|
||||
action: () => emit('openBilling'),
|
||||
shown: props.isStaff,
|
||||
},
|
||||
{
|
||||
id: 'toggle-affiliate',
|
||||
label: props.isAffiliate
|
||||
? formatMessage(messages.removeAffiliateButton)
|
||||
: formatMessage(messages.setAffiliateButton),
|
||||
icon: AffiliateIcon,
|
||||
action: () => emit('toggleAffiliate'),
|
||||
shown: props.isAdmin,
|
||||
remainOnClick: true,
|
||||
color: props.isAffiliate ? 'red' : 'orange',
|
||||
},
|
||||
{
|
||||
id: 'open-info',
|
||||
label: formatMessage(messages.infoButton),
|
||||
icon: InfoIcon,
|
||||
action: () => emit('openInfo'),
|
||||
shown: props.isStaff,
|
||||
},
|
||||
{
|
||||
id: 'open-analytics',
|
||||
label: formatMessage(messages.analyticsButton),
|
||||
icon: ChartIcon,
|
||||
action: () => emit('openAnalytics'),
|
||||
shown: props.isAdmin,
|
||||
},
|
||||
{
|
||||
id: 'edit-role',
|
||||
label: formatMessage(messages.editRoleButton),
|
||||
icon: EditIcon,
|
||||
action: () => emit('editRole'),
|
||||
shown: props.isAdmin,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -72,6 +72,18 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<input
|
||||
id="create-account-consent"
|
||||
v-model="accountConsent"
|
||||
name="account_consent"
|
||||
type="checkbox"
|
||||
tabindex="-1"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label for="create-account-consent">I agree to receive account updates by email</label>
|
||||
</div>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!w-full font-bold"
|
||||
@@ -97,7 +109,7 @@ import {
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
||||
|
||||
@@ -109,7 +121,7 @@ interface AuthGlobals {
|
||||
interface Props {
|
||||
globals?: AuthGlobals | null
|
||||
requiresDob?: boolean
|
||||
onCompleteSignUp?: () => void
|
||||
onCompleteSignUp?: (accountConsent: boolean) => void
|
||||
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
|
||||
}
|
||||
|
||||
@@ -127,6 +139,7 @@ const dateOfBirthModel = defineModel<string | null>('dateOfBirth', { default: ''
|
||||
const usernameModel = defineModel<string>('username', { default: '' })
|
||||
const tokenModel = defineModel<string>('token', { default: '' })
|
||||
const subscribeModel = defineModel<boolean>('subscribe', { default: false })
|
||||
const accountConsent = ref(false)
|
||||
|
||||
const maxInputDate = computed(() => `${new Date().getFullYear()}-12-31`)
|
||||
|
||||
@@ -198,7 +211,7 @@ function onCompleteSignUpClick() {
|
||||
return
|
||||
}
|
||||
|
||||
onCompleteSignUp()
|
||||
onCompleteSignUp(accountConsent.value)
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
@@ -5,16 +5,21 @@
|
||||
<p class="m-0 break-words font-semibold text-contrast">
|
||||
{{ trace.project_name }}
|
||||
</p>
|
||||
<p class="m-0 mt-1 break-all text-sm text-secondary">
|
||||
Project {{ trace.project_slug ?? trace.project_id }} / Version
|
||||
{{ trace.version_number }} / File {{ trace.file_name }}
|
||||
<p class="m-0 mt-1 flex flex-wrap items-center gap-1 text-sm text-secondary">
|
||||
<span class="break-all">{{ trace.version_number }}</span>
|
||||
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
|
||||
<span class="break-all">{{ decodeTracePath(trace.file_name) }}</span>
|
||||
<template v-if="trace.jar">
|
||||
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
|
||||
<span class="break-all">{{ decodeTracePath(trace.jar) }}</span>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm text-secondary">Local</span>
|
||||
<Badge :type="trace.local_status" />
|
||||
<span class="text-sm text-secondary">Effective</span>
|
||||
<Badge :type="trace.effective_status" />
|
||||
<template v-if="trace.local_status !== 'pending'">
|
||||
<span class="text-sm text-secondary">Local</span>
|
||||
<Badge :type="trace.local_status" />
|
||||
</template>
|
||||
<ButtonStyled>
|
||||
<NuxtLink :to="localTraceLink">
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
@@ -23,31 +28,12 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid gap-2 text-sm text-secondary md:grid-cols-2">
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Issue</span>
|
||||
{{ trace.issue_type }}
|
||||
</p>
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Severity</span>
|
||||
{{ trace.severity }}
|
||||
</p>
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Path</span>
|
||||
{{ trace.file_path }}
|
||||
</p>
|
||||
<p v-if="trace.jar" class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">JAR</span>
|
||||
{{ trace.jar }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { ChevronRightIcon, ExternalIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -60,4 +46,12 @@ const localTraceLink = computed(
|
||||
props.trace.detail_id,
|
||||
)}`,
|
||||
)
|
||||
|
||||
function decodeTracePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -42,20 +42,34 @@
|
||||
:key="trace.detail_key"
|
||||
class="universal-card flex flex-col gap-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
||||
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
||||
Trace
|
||||
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
||||
</h2>
|
||||
<span
|
||||
v-if="getLatestLocalTrace(trace)"
|
||||
class="rounded-full border border-solid px-2.5 py-1 text-sm font-medium capitalize"
|
||||
:class="getSeverityBadgeColor(getLatestLocalTrace(trace)?.severity)"
|
||||
>
|
||||
{{ getLatestLocalTrace(trace)?.severity }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="getLatestLocalTrace(trace)" class="mt-1 flex flex-wrap gap-x-3 text-sm">
|
||||
<p class="m-0 break-all text-secondary">
|
||||
<span class="font-semibold text-contrast">Issue</span>
|
||||
{{ getLatestLocalTrace(trace)?.issue_type }}
|
||||
</p>
|
||||
<p class="m-0 break-all text-secondary">
|
||||
<span class="font-semibold text-contrast">Path</span>
|
||||
{{ decodeTracePath(getLatestLocalTrace(trace)?.file_path ?? '') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="m-0 mt-1 text-sm text-secondary">
|
||||
{{ formatTraceCount(trace.local_trace_count) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<div class="flex shrink-0 flex-nowrap items-center gap-2">
|
||||
<Badge :type="trace.verdict" />
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
@@ -70,15 +84,14 @@
|
||||
</div>
|
||||
|
||||
<div v-if="getPreviewLocalTraces(trace).length > 0" class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="getVisibleLocalTraceTotal(trace) > getPreviewLocalTraces(trace).length"
|
||||
class="flex flex-wrap items-center justify-between gap-2"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Showing first {{ getPreviewLocalTraces(trace).length }} of
|
||||
{{ getVisibleLocalTraceTotal(trace) }} local traces
|
||||
Showing {{ getPreviewLocalTraces(trace).length }} of
|
||||
{{ formatTraceCount(getVisibleLocalTraceTotal(trace)) }}
|
||||
</p>
|
||||
<ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="getVisibleLocalTraceTotal(trace) > getPreviewLocalTraces(trace).length"
|
||||
>
|
||||
<NuxtLink :to="getGlobalTraceLink(trace)">
|
||||
<ListIcon aria-hidden="true" />
|
||||
View all
|
||||
@@ -129,7 +142,7 @@ const isLoading = ref(false)
|
||||
const loadError = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 20
|
||||
const localTracePreviewLimit = 10
|
||||
const localTracePreviewLimit = 3
|
||||
const total = ref(0)
|
||||
const traces = ref<Labrinth.TechReview.Internal.GlobalIssueDetail[]>([])
|
||||
const removingTraceKeys = reactive<Set<string>>(new Set())
|
||||
@@ -148,6 +161,34 @@ function getPreviewLocalTraces(trace: Labrinth.TechReview.Internal.GlobalIssueDe
|
||||
return trace.local_traces.slice(0, localTracePreviewLimit)
|
||||
}
|
||||
|
||||
function getLatestLocalTrace(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return trace.local_traces.at(-1)
|
||||
}
|
||||
|
||||
function decodeTracePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
function getSeverityBadgeColor(
|
||||
severity: Labrinth.TechReview.Internal.DelphiSeverity | undefined,
|
||||
): string {
|
||||
switch (severity) {
|
||||
case 'severe':
|
||||
return 'border-red/60 bg-highlight-red text-red'
|
||||
case 'high':
|
||||
return 'border-orange/60 bg-highlight-orange text-orange'
|
||||
case 'medium':
|
||||
return 'border-green/60 bg-highlight-green text-green'
|
||||
case 'low':
|
||||
default:
|
||||
return 'border-blue/60 bg-highlight-blue text-blue'
|
||||
}
|
||||
}
|
||||
|
||||
function getVisibleLocalTraceTotal(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return Math.max(trace.local_trace_count, trace.local_traces.length)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
BanIcon,
|
||||
BugIcon,
|
||||
CheckCheckIcon,
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
ShieldCheckIcon,
|
||||
SpinnerIcon,
|
||||
TimerIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { type TechReviewContext, techReviewQuickReplies } from '@modrinth/moderation'
|
||||
@@ -36,6 +34,7 @@ import {
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
Toggle,
|
||||
useFormatBytes,
|
||||
useFormatDateTime,
|
||||
useVIntl,
|
||||
@@ -247,6 +246,42 @@ function getAllDetails(): Labrinth.TechReview.Internal.ReportIssueDetail[] {
|
||||
return props.item.reports.flatMap((report) => report.issues.flatMap((issue) => issue.details))
|
||||
}
|
||||
|
||||
const hideGloballyPassed = ref(true)
|
||||
|
||||
function isDetailGloballyPassed(detail: Labrinth.TechReview.Internal.ReportIssueDetail): boolean {
|
||||
if (detailDecisionScopes.get(detail.id) === 'global') {
|
||||
return detailDecisions.get(detail.id) === 'safe'
|
||||
}
|
||||
|
||||
return detail.global_status === 'safe'
|
||||
}
|
||||
|
||||
function isDetailGloballyResolved(detail: Labrinth.TechReview.Internal.ReportIssueDetail): boolean {
|
||||
if (detailDecisionScopes.get(detail.id) === 'global') {
|
||||
return detailDecisions.get(detail.id) !== 'pending'
|
||||
}
|
||||
|
||||
return detail.global_status === 'safe' || detail.global_status === 'unsafe'
|
||||
}
|
||||
|
||||
const globallyPassedSelectedFileCount = computed(() => {
|
||||
if (!selectedFile.value) return 0
|
||||
|
||||
return selectedFile.value.issues.reduce(
|
||||
(count, issue) => count + issue.details.filter(isDetailGloballyPassed).length,
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
const globallyResolvedSelectedFileCount = computed(() => {
|
||||
if (!selectedFile.value) return 0
|
||||
|
||||
return selectedFile.value.issues.reduce(
|
||||
(count, issue) => count + issue.details.filter(isDetailGloballyResolved).length,
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
function applyDecisionToRelatedDetails(
|
||||
detailIds: string[],
|
||||
decision: DetailDecision,
|
||||
@@ -397,10 +432,7 @@ watch(
|
||||
allFiles.value = [...reports].sort((a, b) => {
|
||||
const aComplete = getFileMarkedCount(a) === getFileDetailCount(a)
|
||||
const bComplete = getFileMarkedCount(b) === getFileDetailCount(b)
|
||||
if (aComplete !== bComplete) return aComplete ? 1 : -1
|
||||
const aSeverity = getFileHighestSeverity(a)
|
||||
const bSeverity = getFileHighestSeverity(b)
|
||||
return (severityOrder[bSeverity] ?? 0) - (severityOrder[aSeverity] ?? 0)
|
||||
return aComplete === bComplete ? 0 : aComplete ? 1 : -1
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -615,6 +647,18 @@ const remainingUnmarkedCount = computed(() => {
|
||||
return getFileDetailCount(selectedFile.value) - getFileMarkedCount(selectedFile.value)
|
||||
})
|
||||
|
||||
function getSelectedFileFlags(): ClassGroup['flags'] {
|
||||
if (!selectedFile.value) return []
|
||||
|
||||
return selectedFile.value.issues.flatMap((issue) =>
|
||||
issue.details.map((detail) => ({
|
||||
issueId: issue.id,
|
||||
issueType: issue.issue_type,
|
||||
detail,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
function getJarFlags(jarGroup: JarGroup): ClassGroup['flags'] {
|
||||
return jarGroup.classes.flatMap((classItem) => classItem.flags)
|
||||
}
|
||||
@@ -629,6 +673,74 @@ function getJarRemainingUnmarkedCount(jarGroup: JarGroup): number {
|
||||
|
||||
const isBatchUpdating = ref(false)
|
||||
|
||||
function getRemainingGlobalDetailCount(flags: ClassGroup['flags']): number {
|
||||
return new Set(
|
||||
flags
|
||||
.filter(
|
||||
(flag) =>
|
||||
getDetailDecision(flag.detail.id, flag.detail.status) === 'pending' &&
|
||||
canUpdateGlobalDetail(flag.detail),
|
||||
)
|
||||
.map((flag) => flag.detail.key),
|
||||
).size
|
||||
}
|
||||
|
||||
async function batchMarkRemainingGlobally(flags: ClassGroup['flags'], verdict: 'safe' | 'unsafe') {
|
||||
if (isBatchUpdating.value) return
|
||||
|
||||
const detailsByKey = new Map(
|
||||
flags
|
||||
.filter(
|
||||
(flag) =>
|
||||
getDetailDecision(flag.detail.id, flag.detail.status) === 'pending' &&
|
||||
canUpdateGlobalDetail(flag.detail),
|
||||
)
|
||||
.map((flag) => [flag.detail.key, flag.detail]),
|
||||
)
|
||||
const details = [...detailsByKey.values()]
|
||||
|
||||
if (details.length === 0) return
|
||||
|
||||
isBatchUpdating.value = true
|
||||
try {
|
||||
await client.labrinth.tech_review_internal.updateGlobalIssueDetails(
|
||||
details.map((detail) => ({ detail_key: detail.key, verdict })),
|
||||
)
|
||||
|
||||
applyDecisionToRelatedDetails(
|
||||
details.map((detail) => detail.id),
|
||||
verdictToDecision(verdict),
|
||||
'global',
|
||||
)
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: `Globally marked ${details.length} trace keys as ${verdict}`,
|
||||
text: `All remaining eligible traces have been globally marked as ${
|
||||
verdict === 'safe' ? 'false positives' : 'malicious'
|
||||
}.`,
|
||||
})
|
||||
|
||||
if (
|
||||
selectedFile.value &&
|
||||
getFileMarkedCount(selectedFile.value) === getFileDetailCount(selectedFile.value)
|
||||
) {
|
||||
backToFileList()
|
||||
}
|
||||
|
||||
emit('refetch')
|
||||
} catch (error) {
|
||||
console.error('Failed to batch update global traces:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Global batch update failed',
|
||||
text: 'An error occurred while globally updating traces.',
|
||||
})
|
||||
} finally {
|
||||
isBatchUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function batchMarkRemaining(verdict: 'safe' | 'unsafe') {
|
||||
if (!selectedFile.value || isBatchUpdating.value) return
|
||||
|
||||
@@ -913,7 +1025,7 @@ interface JarGroup {
|
||||
function splitJarSegments(jar: string | null, currentFileName: string | null): string[] {
|
||||
if (!jar) return []
|
||||
const segments = jar
|
||||
.split('#')
|
||||
.split(/[/#]/)
|
||||
.map((s) => decodeURIComponent(s.trim()))
|
||||
.filter((s) => s.length > 0)
|
||||
// Skip the first segment if it matches the current file tab (it's already shown in the file list)
|
||||
@@ -934,6 +1046,10 @@ const groupedByClass = computed<ClassGroup[]>(() => {
|
||||
|
||||
for (const issue of selectedFile.value.issues) {
|
||||
for (const detail of issue.details) {
|
||||
if (hideGloballyPassed.value && isDetailGloballyPassed(detail)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const classKey = `${detail.jar ?? ''}::${detail.file_path}`
|
||||
if (!classMap.has(classKey)) {
|
||||
classMap.set(classKey, {
|
||||
@@ -959,20 +1075,11 @@ const groupedByClass = computed<ClassGroup[]>(() => {
|
||||
classGroup.flags.sort((a, b) => {
|
||||
const aPreReviewed = isPreReviewed(a.detail.id, a.detail.status)
|
||||
const bPreReviewed = isPreReviewed(b.detail.id, b.detail.status)
|
||||
|
||||
if (aPreReviewed !== bPreReviewed) {
|
||||
return aPreReviewed ? 1 : -1
|
||||
}
|
||||
|
||||
return (severityOrder[b.detail.severity] ?? 0) - (severityOrder[a.detail.severity] ?? 0)
|
||||
return aPreReviewed === bPreReviewed ? 0 : aPreReviewed ? 1 : -1
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(classMap.values()).sort((a, b) => {
|
||||
const aSeverity = getHighestSeverityInClass(a.flags)
|
||||
const bSeverity = getHighestSeverityInClass(b.flags)
|
||||
return (severityOrder[bSeverity] ?? 0) - (severityOrder[aSeverity] ?? 0)
|
||||
})
|
||||
return Array.from(classMap.values())
|
||||
})
|
||||
|
||||
const groupedByJar = computed<JarGroup[]>(() => {
|
||||
@@ -994,11 +1101,7 @@ const groupedByJar = computed<JarGroup[]>(() => {
|
||||
return Array.from(jarMap.values()).sort((a, b) => {
|
||||
const aRoot = isRootJarGroup(a)
|
||||
const bRoot = isRootJarGroup(b)
|
||||
if (aRoot !== bRoot) return aRoot ? -1 : 1
|
||||
|
||||
const aSeverity = getHighestSeverityInClass(a.classes.flatMap((classItem) => classItem.flags))
|
||||
const bSeverity = getHighestSeverityInClass(b.classes.flatMap((classItem) => classItem.flags))
|
||||
return (severityOrder[bSeverity] ?? 0) - (severityOrder[aSeverity] ?? 0)
|
||||
return aRoot === bRoot ? 0 : aRoot ? -1 : 1
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1589,21 +1692,73 @@ function copyId() {
|
||||
|
||||
<template v-else-if="currentTab === 'File' && selectedFile">
|
||||
<div
|
||||
v-if="remainingUnmarkedCount > 0"
|
||||
class="flex gap-2 border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2 p-4"
|
||||
v-if="getFileDetailCount(selectedFile) > 0"
|
||||
class="flex flex-wrap items-center justify-between gap-3 border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2 p-4"
|
||||
>
|
||||
<ButtonStyled color="brand" :disabled="isBatchUpdating">
|
||||
<button @click="batchMarkRemaining('safe')">
|
||||
<CheckCircleIcon class="size-5" />
|
||||
Remaining safe ({{ remainingUnmarkedCount }})
|
||||
<div
|
||||
v-if="remainingUnmarkedCount > 0"
|
||||
class="detail-verdict-buttons"
|
||||
role="group"
|
||||
aria-label="Remaining issue actions"
|
||||
>
|
||||
<span class="remaining-verdict-label"
|
||||
>Remaining issues ({{ remainingUnmarkedCount }})</span
|
||||
>
|
||||
<button
|
||||
v-tooltip="'Remaining globally safe'"
|
||||
class="detail-verdict-button detail-verdict-button--safe"
|
||||
aria-label="Remaining globally safe"
|
||||
:disabled="
|
||||
isBatchUpdating || getRemainingGlobalDetailCount(getSelectedFileFlags()) === 0
|
||||
"
|
||||
@click="batchMarkRemainingGlobally(getSelectedFileFlags(), 'safe')"
|
||||
>
|
||||
<CheckCheckIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" :disabled="isBatchUpdating">
|
||||
<button @click="batchMarkRemaining('unsafe')">
|
||||
<TriangleAlertIcon class="size-5" />
|
||||
Remaining malware ({{ remainingUnmarkedCount }})
|
||||
<button
|
||||
v-tooltip="'Remaining safe'"
|
||||
class="detail-verdict-button detail-verdict-button--safe"
|
||||
aria-label="Remaining safe"
|
||||
:disabled="isBatchUpdating"
|
||||
@click="batchMarkRemaining('safe')"
|
||||
>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<button
|
||||
v-tooltip="'Remaining malware'"
|
||||
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||
aria-label="Remaining malware"
|
||||
:disabled="isBatchUpdating"
|
||||
@click="batchMarkRemaining('unsafe')"
|
||||
>
|
||||
<BanIcon aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="'Remaining globally unsafe'"
|
||||
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||
aria-label="Remaining globally unsafe"
|
||||
:disabled="
|
||||
isBatchUpdating || getRemainingGlobalDetailCount(getSelectedFileFlags()) === 0
|
||||
"
|
||||
@click="batchMarkRemainingGlobally(getSelectedFileFlags(), 'unsafe')"
|
||||
>
|
||||
<ShieldAlertIcon aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<label class="ml-auto flex cursor-pointer items-center gap-3 text-sm">
|
||||
<span class="text-right text-secondary">
|
||||
Hide globally passed
|
||||
<span class="text-tertiary block text-xs">
|
||||
{{ globallyResolvedSelectedFileCount }}/{{ getFileDetailCount(selectedFile) }}
|
||||
traces globally resolved
|
||||
</span>
|
||||
</span>
|
||||
<Toggle
|
||||
v-model="hideGloballyPassed"
|
||||
:disabled="globallyPassedSelectedFileCount === 0"
|
||||
small
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="jarGroup in groupedByJar"
|
||||
@@ -1637,25 +1792,55 @@ function copyId() {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="getJarRemainingUnmarkedCount(jarGroup) > 0" class="flex gap-2">
|
||||
<ButtonStyled color="brand" size="small">
|
||||
<button
|
||||
:disabled="isBatchUpdating"
|
||||
@click="batchMarkRemainingInJar(jarGroup, 'safe')"
|
||||
>
|
||||
<CheckCircleIcon class="size-4" />
|
||||
Remaining safe ({{ getJarRemainingUnmarkedCount(jarGroup) }})
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" size="small">
|
||||
<button
|
||||
:disabled="isBatchUpdating"
|
||||
@click="batchMarkRemainingInJar(jarGroup, 'unsafe')"
|
||||
>
|
||||
<TriangleAlertIcon class="size-4" />
|
||||
Remaining malware ({{ getJarRemainingUnmarkedCount(jarGroup) }})
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div
|
||||
v-if="getJarRemainingUnmarkedCount(jarGroup) > 0"
|
||||
class="detail-verdict-buttons"
|
||||
role="group"
|
||||
aria-label="Remaining JAR issue actions"
|
||||
>
|
||||
<span class="remaining-verdict-label">
|
||||
Remaining issues ({{ getJarRemainingUnmarkedCount(jarGroup) }})
|
||||
</span>
|
||||
<button
|
||||
v-tooltip="'Remaining globally safe'"
|
||||
class="detail-verdict-button detail-verdict-button--safe"
|
||||
aria-label="Remaining globally safe"
|
||||
:disabled="
|
||||
isBatchUpdating || getRemainingGlobalDetailCount(getJarFlags(jarGroup)) === 0
|
||||
"
|
||||
@click="batchMarkRemainingGlobally(getJarFlags(jarGroup), 'safe')"
|
||||
>
|
||||
<CheckCheckIcon aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="'Remaining safe'"
|
||||
class="detail-verdict-button detail-verdict-button--safe"
|
||||
aria-label="Remaining safe"
|
||||
:disabled="isBatchUpdating"
|
||||
@click="batchMarkRemainingInJar(jarGroup, 'safe')"
|
||||
>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="'Remaining malware'"
|
||||
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||
aria-label="Remaining malware"
|
||||
:disabled="isBatchUpdating"
|
||||
@click="batchMarkRemainingInJar(jarGroup, 'unsafe')"
|
||||
>
|
||||
<BanIcon aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="'Remaining globally unsafe'"
|
||||
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||
aria-label="Remaining globally unsafe"
|
||||
:disabled="
|
||||
isBatchUpdating || getRemainingGlobalDetailCount(getJarFlags(jarGroup)) === 0
|
||||
"
|
||||
@click="batchMarkRemainingGlobally(getJarFlags(jarGroup), 'unsafe')"
|
||||
>
|
||||
<ShieldAlertIcon aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1968,6 +2153,14 @@ pre {
|
||||
background: var(--surface-3);
|
||||
}
|
||||
|
||||
.remaining-verdict-label {
|
||||
padding-inline: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.detail-verdict-button {
|
||||
display: flex;
|
||||
width: 2rem;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { FolderSearchIcon, StarIcon } from '@modrinth/assets'
|
||||
import { FolderSearchIcon, StarIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
Table,
|
||||
type TableColumn,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -65,6 +67,10 @@ const messages = defineMessages({
|
||||
id: 'modpack-scan-modal.scan-error',
|
||||
defaultMessage: 'Some files failed to scan: {error}',
|
||||
},
|
||||
clearAllGroups: {
|
||||
id: 'modpack-scan-modal.clear-all-groups',
|
||||
defaultMessage: 'Clear All Groups',
|
||||
},
|
||||
})
|
||||
|
||||
type ScanTableColumn = 'filename' | 'newFiles' | 'newGroups'
|
||||
@@ -85,12 +91,15 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const rows = ref<ScanRow[]>([])
|
||||
const isLoadingVersions = ref(false)
|
||||
const isScanning = ref(false)
|
||||
const isClearing = ref(false)
|
||||
const versionLoadError = ref<string | null>(null)
|
||||
const scanError = ref<string | null>(null)
|
||||
const requestId = ref(0)
|
||||
@@ -103,7 +112,7 @@ const columns = computed<TableColumn<ScanTableColumn>[]>(() => [
|
||||
])
|
||||
|
||||
const scannedCount = computed(() => rows.value.filter((row) => row.scan || row.error).length)
|
||||
const isBusy = computed(() => isLoadingVersions.value || isScanning.value)
|
||||
const isBusy = computed(() => isLoadingVersions.value || isScanning.value || isClearing.value)
|
||||
|
||||
function getErrorMessage(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
@@ -208,6 +217,44 @@ async function fetchAllScans() {
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAllGroups() {
|
||||
if (isBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
let failed = false
|
||||
|
||||
try {
|
||||
isClearing.value = true
|
||||
const groups = await client.labrinth.attribution_internal.listProjectAttribution(
|
||||
props.project_id,
|
||||
)
|
||||
|
||||
for (const group of groups) {
|
||||
await client.labrinth.attribution_internal.deleteGroup(group.id)
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ['project-attribution', props.project_id] })
|
||||
} catch (error) {
|
||||
failed = true
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'An error occurred',
|
||||
text: `Failed to clear all groups: ${getErrorMessage(error)}`,
|
||||
})
|
||||
} finally {
|
||||
isClearing.value = false
|
||||
}
|
||||
|
||||
if (!failed) {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: 'All groups cleared successfully.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
scanRequestId.value++
|
||||
isScanning.value = false
|
||||
@@ -240,7 +287,16 @@ defineExpose({ show, hide })
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.clearAllGroups)"
|
||||
:disabled="isBusy || rows.length === 0"
|
||||
@click="clearAllGroups"
|
||||
>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.scanAllFiles)"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,780 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
BooleanNodeBuilder,
|
||||
ButtonNodeBuilder,
|
||||
ChildNode,
|
||||
DropdownNodeBuilder,
|
||||
GroupNodeBuilder,
|
||||
IdentifiedNodeBuilder,
|
||||
InputNodeBuilder,
|
||||
LabeledNodeBuilder,
|
||||
NodeState,
|
||||
NodeStateWithChildren,
|
||||
OverrideValue,
|
||||
ValueNodeBuilder,
|
||||
} from '@modrinth/moderation'
|
||||
import {
|
||||
evalSegment,
|
||||
expandVariables,
|
||||
flattenProjectV3Variables,
|
||||
flattenProjectVariables,
|
||||
flattenStaticVariables,
|
||||
getBooleanChildState,
|
||||
NodeBuilder,
|
||||
resolve,
|
||||
resolveChildren,
|
||||
setMessageProject,
|
||||
} from '@modrinth/moderation'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
injectProjectPageContext,
|
||||
MarkdownEditor,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import { renderHighlightedString, renderString } from '@modrinth/utils'
|
||||
import { inject, nextTick, onMounted, reactive, watchEffect } from 'vue'
|
||||
|
||||
import { NODE_META_KEY, STATE_KEY } from './checklist-context'
|
||||
|
||||
const nodeMetaMap = inject(NODE_META_KEY)
|
||||
const injectedGlobalState = inject(STATE_KEY)
|
||||
|
||||
const { projectV3: project, projectV2 } = injectProjectPageContext()
|
||||
setMessageProject(project, projectV2)
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: ChildNode[]
|
||||
showContext: Record<string, NodeState>
|
||||
onImageUpload?: (file: File) => Promise<string>
|
||||
flex?: boolean
|
||||
titleDepth?: number
|
||||
parentStatePath?: string[]
|
||||
}>()
|
||||
|
||||
function titleClass(depth: number): string {
|
||||
if (depth === 0) return 'text-lg font-extrabold text-contrast'
|
||||
if (depth === 1) return 'text-base font-semibold'
|
||||
if (depth === 2) return 'text-sm font-semibold'
|
||||
return ''
|
||||
}
|
||||
|
||||
function isVisible(node: NodeBuilder): boolean {
|
||||
if (node._shown !== undefined) return resolve(node._shown)
|
||||
if (node.type === 'group') {
|
||||
const children = getChildren(node as IdentifiedNodeBuilder)
|
||||
return children.some((c) => !(c instanceof NodeBuilder) || isVisible(c))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isEnabled(node: IdentifiedNodeBuilder): boolean {
|
||||
const e = node._enabled
|
||||
if (e === undefined) return true
|
||||
if (typeof e === 'function') return e(props.showContext)
|
||||
return resolve(e)
|
||||
}
|
||||
|
||||
function isButtonEnabled(node: ButtonNodeBuilder): boolean {
|
||||
const enabled = node._enabled
|
||||
if (enabled === undefined) return true
|
||||
if (typeof enabled === 'function') return enabled(props.showContext)
|
||||
return resolve(enabled)
|
||||
}
|
||||
|
||||
function asBool(node: NodeBuilder): BooleanNodeBuilder {
|
||||
return node as BooleanNodeBuilder
|
||||
}
|
||||
|
||||
function asButton(node: NodeBuilder): ButtonNodeBuilder {
|
||||
return node as ButtonNodeBuilder
|
||||
}
|
||||
|
||||
function asIdentified(node: NodeBuilder): IdentifiedNodeBuilder {
|
||||
return node as IdentifiedNodeBuilder
|
||||
}
|
||||
|
||||
function asLabeled(node: NodeBuilder): LabeledNodeBuilder {
|
||||
return node as LabeledNodeBuilder
|
||||
}
|
||||
|
||||
function asGroup(node: NodeBuilder): GroupNodeBuilder {
|
||||
return node as GroupNodeBuilder
|
||||
}
|
||||
|
||||
function asDropdown(node: NodeBuilder): DropdownNodeBuilder {
|
||||
return node as DropdownNodeBuilder
|
||||
}
|
||||
|
||||
function asInput(node: NodeBuilder): InputNodeBuilder {
|
||||
return node as InputNodeBuilder
|
||||
}
|
||||
|
||||
function getAtPath(path: string[]): NodeState {
|
||||
let current: unknown = injectedGlobalState!.value
|
||||
for (const key of path) {
|
||||
if (current == null || typeof current !== 'object' || current instanceof Set) return undefined
|
||||
current = (current as Record<string, unknown>)[key]
|
||||
}
|
||||
return current as NodeState
|
||||
}
|
||||
|
||||
function setAtPath(path: string[], value: NodeState): void {
|
||||
if (path.length === 0) return
|
||||
const global = injectedGlobalState!.value as unknown as Record<string, unknown>
|
||||
let current = global
|
||||
const stack: [Record<string, unknown>, string][] = []
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const key = path[i]
|
||||
const next = current[key]
|
||||
if (!next || typeof next !== 'object' || next instanceof Set) {
|
||||
if (value === undefined) return
|
||||
current[key] = next !== null && next !== undefined ? { value: next } : {}
|
||||
current = current[key] as Record<string, unknown>
|
||||
} else {
|
||||
stack.push([current, key])
|
||||
current = next as Record<string, unknown>
|
||||
}
|
||||
}
|
||||
const lastKey = path[path.length - 1]
|
||||
if (value === undefined) {
|
||||
Reflect.deleteProperty(current, lastKey)
|
||||
for (let i = stack.length - 1; i >= 0; i--) {
|
||||
const [parent, key] = stack[i]
|
||||
const child = parent[key]
|
||||
if (
|
||||
child &&
|
||||
typeof child === 'object' &&
|
||||
!(child instanceof Set) &&
|
||||
Object.keys(child as object).length === 0
|
||||
) {
|
||||
Reflect.deleteProperty(parent, key)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current[lastKey] = value as unknown
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeState(node: IdentifiedNodeBuilder): NodeState {
|
||||
return node._statePath ? getAtPath(node._statePath) : undefined
|
||||
}
|
||||
|
||||
function setNodeState(node: IdentifiedNodeBuilder, value: NodeState): void {
|
||||
if (node._statePath) setAtPath(node._statePath, value)
|
||||
}
|
||||
|
||||
function getBooleanState(node: BooleanNodeBuilder): boolean {
|
||||
const state = getNodeState(node)
|
||||
if (typeof state === 'boolean') return state
|
||||
if (state && typeof state === 'object' && !(state instanceof Set)) {
|
||||
const v = (state as NodeStateWithChildren).value
|
||||
if (typeof v === 'boolean') return v
|
||||
}
|
||||
const def = resolveDefault(node)
|
||||
return (def as boolean | undefined) ?? false
|
||||
}
|
||||
|
||||
function getMultiSelectState(node: IdentifiedNodeBuilder): Set<string> {
|
||||
const state = getNodeState(node)
|
||||
return state instanceof Set ? state : new Set<string>()
|
||||
}
|
||||
|
||||
function getSelectState(node: IdentifiedNodeBuilder): string | undefined {
|
||||
const state = getNodeState(node)
|
||||
if (typeof state === 'string') return state
|
||||
const def = resolveDefault(node as ValueNodeBuilder)
|
||||
return typeof def === 'string' ? def : undefined
|
||||
}
|
||||
|
||||
function getDropdownOptions(node: DropdownNodeBuilder) {
|
||||
return [
|
||||
...(node._none !== undefined ? [{ value: '', label: node._none }] : []),
|
||||
...visibleChildren(node).map((c) => ({
|
||||
value: asIdentified(c).id!,
|
||||
label: asLabeled(c).label,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
function getDropdownModelValue(node: DropdownNodeBuilder) {
|
||||
return getSelectState(node) ?? (node._none !== undefined ? '' : undefined)
|
||||
}
|
||||
|
||||
function toggleSelect(parent: IdentifiedNodeBuilder, child: IdentifiedNodeBuilder) {
|
||||
const current = getSelectState(parent)
|
||||
setNodeState(parent, current === child.id ? undefined : child.id)
|
||||
}
|
||||
|
||||
function resolveDefault(node: ValueNodeBuilder): NodeState {
|
||||
const d = node._defaultValue
|
||||
return typeof d === 'function' ? d(props.showContext) : d
|
||||
}
|
||||
|
||||
function getTextState(node: IdentifiedNodeBuilder): string {
|
||||
const state = getNodeState(node)
|
||||
if (typeof state === 'string') return state
|
||||
const def = resolveDefault(node as ValueNodeBuilder)
|
||||
return typeof def === 'string' ? def : ''
|
||||
}
|
||||
|
||||
function getNodeTitle(node: NodeBuilder): string | undefined {
|
||||
if (node._title === undefined) return undefined
|
||||
return resolve(node._title) || undefined
|
||||
}
|
||||
|
||||
function getPlaceholder(node: InputNodeBuilder): string | undefined {
|
||||
if (node._placeholder !== undefined) return resolve(node._placeholder)
|
||||
const def = resolveDefault(node)
|
||||
if (typeof def === 'string') return def
|
||||
return undefined
|
||||
}
|
||||
|
||||
function hasActionableFixes(node: IdentifiedNodeBuilder): boolean {
|
||||
return nodeMetaMap?.value.get(node)?.isFixActionable ?? false
|
||||
}
|
||||
|
||||
function hasRequiredMissingDescendants(node: IdentifiedNodeBuilder): boolean {
|
||||
for (const child of getChildren(node)) {
|
||||
if (!(child instanceof NodeBuilder)) continue
|
||||
const identified = child as IdentifiedNodeBuilder
|
||||
if (nodeMetaMap?.value.get(identified)?.hasRequiredMissing) return true
|
||||
if (identified.id !== undefined && hasRequiredMissingDescendants(identified)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function nodeHasRequiredMissing(node: IdentifiedNodeBuilder): boolean {
|
||||
return !!nodeMetaMap?.value.get(node)?.hasRequiredMissing || hasRequiredMissingDescendants(node)
|
||||
}
|
||||
|
||||
function getBooleanColor(node: BooleanNodeBuilder): string {
|
||||
if (!getBooleanState(node)) return 'standard'
|
||||
if (hasRequiredMissingDescendants(node)) return 'orange'
|
||||
return hasActionableFixes(node) ? 'blue' : 'brand'
|
||||
}
|
||||
|
||||
function setTextState(node: IdentifiedNodeBuilder, v: string): void {
|
||||
const inputNode = node as InputNodeBuilder
|
||||
const result = inputNode._onChange?.(v, overrideHelpers)
|
||||
|
||||
if (isOverrideValue(result)) {
|
||||
const ov = result.__override
|
||||
const def = resolveDefault(node as ValueNodeBuilder)
|
||||
const defStr = typeof def === 'string' ? def : ''
|
||||
setNodeState(node, ov === defStr ? undefined : ov || undefined)
|
||||
nextTick(() => textInputRefs.get(node)?.setValue(ov))
|
||||
return
|
||||
}
|
||||
|
||||
const def = resolveDefault(node as ValueNodeBuilder)
|
||||
const defStr = typeof def === 'string' ? def : ''
|
||||
setNodeState(node, v === defStr ? undefined : defStr ? v : v || undefined)
|
||||
}
|
||||
|
||||
function handleButtonClick(node: ButtonNodeBuilder): void {
|
||||
const before = new Map<NodeBuilder, string>()
|
||||
for (const inputNode of textInputRefs.keys()) {
|
||||
before.set(inputNode, getTextState(asIdentified(inputNode)))
|
||||
}
|
||||
node._onClick?.(props.showContext)
|
||||
for (const [inputNode, beforeVal] of before) {
|
||||
const after = getTextState(asIdentified(inputNode))
|
||||
if (after !== beforeVal) setTextState(asIdentified(inputNode), after)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
for (const inputNode of textInputRefs.keys()) {
|
||||
setTextState(asIdentified(inputNode), getTextState(asIdentified(inputNode)))
|
||||
}
|
||||
})
|
||||
|
||||
function toggleBoolean(node: BooleanNodeBuilder) {
|
||||
const raw = getNodeState(node)
|
||||
const next = !getBooleanState(node)
|
||||
const defaultVal = (resolveDefault(node) as boolean | undefined) ?? false
|
||||
const isDefault = next === defaultVal
|
||||
if (raw && typeof raw === 'object' && !(raw instanceof Set)) {
|
||||
const { value: _v, ...children } = raw as NodeStateWithChildren & Record<string, NodeState>
|
||||
const hasChildren = Object.keys(children).length > 0
|
||||
setNodeState(
|
||||
node,
|
||||
isDefault && !hasChildren
|
||||
? undefined
|
||||
: ({ ...children, ...(isDefault ? {} : { value: next }) } as NodeState),
|
||||
)
|
||||
} else {
|
||||
setNodeState(node, isDefault ? undefined : next)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChip(parent: IdentifiedNodeBuilder, child: IdentifiedNodeBuilder) {
|
||||
const selected = new Set(getMultiSelectState(parent))
|
||||
if (selected.has(child.id!)) {
|
||||
selected.delete(child.id!)
|
||||
if (child._statePath) {
|
||||
setAtPath(child._statePath, undefined)
|
||||
}
|
||||
} else {
|
||||
selected.add(child.id!)
|
||||
}
|
||||
setNodeState(parent, selected.size > 0 ? selected : undefined)
|
||||
}
|
||||
|
||||
const textInputRefs = new Map<NodeBuilder, { setValue: (v: string) => void }>()
|
||||
const overrideHelpers = { override: (v: string): OverrideValue => ({ __override: v }) }
|
||||
|
||||
function isOverrideValue(v: unknown): v is OverrideValue {
|
||||
return v !== null && typeof v === 'object' && '__override' in v
|
||||
}
|
||||
|
||||
const scopedContextFallbacks = new WeakMap<IdentifiedNodeBuilder, Record<string, NodeState>>()
|
||||
|
||||
function childScopedContext(child: IdentifiedNodeBuilder): Record<string, NodeState> {
|
||||
if (!child._statePath) return props.showContext
|
||||
const basePath = child._statePath
|
||||
const state = getAtPath(basePath)
|
||||
if (state && typeof state === 'object' && !(state instanceof Set)) {
|
||||
return state as Record<string, NodeState>
|
||||
}
|
||||
const existing = scopedContextFallbacks.get(child)
|
||||
if (existing) return existing
|
||||
const fallback = new Proxy({} as Record<string, NodeState>, {
|
||||
set(_target, key, value) {
|
||||
setAtPath([...basePath, key as string], value as NodeState)
|
||||
return true
|
||||
},
|
||||
})
|
||||
scopedContextFallbacks.set(child, fallback)
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getChildrenContext(node: IdentifiedNodeBuilder): Record<string, NodeState> {
|
||||
if (node.type === 'dropdown') return props.showContext
|
||||
if (node.type === 'group' && asGroup(node)._selectMode) return props.showContext
|
||||
return childScopedContext(node)
|
||||
}
|
||||
|
||||
function getChildren(node: IdentifiedNodeBuilder): ChildNode[] {
|
||||
return resolveChildren(node, getChildrenContext(node))
|
||||
}
|
||||
|
||||
function visibleChildren(node: IdentifiedNodeBuilder): NodeBuilder[] {
|
||||
return getChildren(node).filter((c): c is NodeBuilder => c instanceof NodeBuilder && isVisible(c))
|
||||
}
|
||||
|
||||
const tooltipHtml = reactive(new Map<NodeBuilder, string>())
|
||||
|
||||
function getTooltipConfig(node: NodeBuilder, state?: Record<string, NodeState>) {
|
||||
const t = node._tooltip
|
||||
const manual = t === undefined ? undefined : typeof t === 'function' ? t(state ?? {}) : resolve(t)
|
||||
if (manual)
|
||||
return {
|
||||
content: manual,
|
||||
delay: { show: 500, hide: 0 },
|
||||
triggers: ['hover', 'focus'],
|
||||
placement: 'top',
|
||||
}
|
||||
const html = tooltipHtml.get(node)
|
||||
if (!html) return undefined
|
||||
return {
|
||||
content: html,
|
||||
html: true,
|
||||
delay: { show: 500, hide: 0 },
|
||||
triggers: ['hover', 'focus'],
|
||||
placement: 'top',
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(async () => {
|
||||
// Read all reactive state synchronously before any await so Vue tracks dependencies
|
||||
const buttonTasks: Array<{ node: BooleanNodeBuilder; state: Record<string, NodeState> }> = []
|
||||
|
||||
for (const node of props.nodes) {
|
||||
if (!(node instanceof NodeBuilder)) continue
|
||||
if (node.type === 'toggle' && isVisible(node)) {
|
||||
const boolNode = asBool(node)
|
||||
if (boolNode._segments.some((s) => s.type !== 'collect')) {
|
||||
const nodeState = getNodeState(boolNode)
|
||||
const childState =
|
||||
nodeState && typeof nodeState === 'object' && !(nodeState instanceof Set)
|
||||
? (() => {
|
||||
const { value: _v, ...rest } = nodeState as NodeStateWithChildren &
|
||||
Record<string, NodeState>
|
||||
return rest
|
||||
})()
|
||||
: {}
|
||||
buttonTasks.push({ node: boolNode, state: childState })
|
||||
}
|
||||
}
|
||||
if (node.type === 'group' && asGroup(node)._selectMode === 'multi' && isVisible(node)) {
|
||||
for (const child of visibleChildren(asIdentified(node))) {
|
||||
const opt = child as IdentifiedNodeBuilder
|
||||
if (opt._segments.some((s) => s.type !== 'collect')) {
|
||||
const childState = getBooleanChildState(getNodeState(opt)) as Record<string, NodeState>
|
||||
buttonTasks.push({ node: opt as BooleanNodeBuilder, state: childState })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function evalCollectedChildren(node: IdentifiedNodeBuilder): Promise<string> {
|
||||
let result = ''
|
||||
for (const child of getChildren(node)) {
|
||||
if (!(child instanceof NodeBuilder)) continue
|
||||
if (!isVisible(child)) continue
|
||||
const childNode = asIdentified(child)
|
||||
if (child.type === 'group') {
|
||||
const grp = asGroup(childNode)
|
||||
if (grp._selectMode === 'multi') {
|
||||
const selected = getMultiSelectState(childNode)
|
||||
for (const opt of getChildren(childNode)) {
|
||||
if (!(opt instanceof NodeBuilder) || !isVisible(opt)) continue
|
||||
const optNode = asIdentified(opt)
|
||||
if (!optNode.id || !selected.has(optNode.id)) continue
|
||||
result += await evalNodeTooltip(
|
||||
optNode,
|
||||
getBooleanChildState(getNodeState(optNode)) as Record<string, NodeState>,
|
||||
)
|
||||
}
|
||||
} else if (grp._selectMode === 'single') {
|
||||
const selected = getSelectState(childNode)
|
||||
for (const opt of getChildren(childNode)) {
|
||||
if (!(opt instanceof NodeBuilder) || !isVisible(opt)) continue
|
||||
const optNode = asIdentified(opt)
|
||||
if (optNode.id !== selected) continue
|
||||
result += await evalNodeTooltip(
|
||||
optNode,
|
||||
getBooleanChildState(getNodeState(optNode)) as Record<string, NodeState>,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
result += await evalCollectedChildren(childNode)
|
||||
}
|
||||
} else if (child.type === 'dropdown') {
|
||||
const selected = getSelectState(childNode)
|
||||
for (const opt of getChildren(childNode)) {
|
||||
if (!(opt instanceof NodeBuilder) || !isVisible(opt)) continue
|
||||
const optNode = asIdentified(opt)
|
||||
if (optNode.id !== selected) continue
|
||||
result += await evalNodeTooltip(
|
||||
optNode,
|
||||
getBooleanChildState(getNodeState(optNode)) as Record<string, NodeState>,
|
||||
)
|
||||
}
|
||||
} else if (child.type === 'toggle' || child.type === 'check') {
|
||||
if (!getBooleanState(asBool(childNode))) continue
|
||||
result += await evalNodeTooltip(
|
||||
childNode,
|
||||
getBooleanChildState(getNodeState(childNode)) as Record<string, NodeState>,
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function evalNodeTooltip(
|
||||
node: IdentifiedNodeBuilder,
|
||||
state: Record<string, NodeState>,
|
||||
): Promise<string> {
|
||||
let result = ''
|
||||
for (const seg of node._segments) {
|
||||
if (seg.type === 'collect') {
|
||||
let collected = await evalCollectedChildren(node)
|
||||
if (!collected.trim() && seg.fallback) {
|
||||
collected = await evalSegment(seg.fallback, state, node._statePath ?? [])
|
||||
}
|
||||
result += collected
|
||||
} else {
|
||||
result += await evalSegment(seg, state, node._statePath ?? [])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
for (const { node, state } of buttonTasks) {
|
||||
try {
|
||||
const raw = await evalNodeTooltip(node as unknown as IdentifiedNodeBuilder, state)
|
||||
const expanded = expandVariables(raw, projectV2.value, project.value, {
|
||||
...flattenStaticVariables(),
|
||||
...flattenProjectVariables(projectV2.value),
|
||||
...flattenProjectV3Variables(project.value),
|
||||
})
|
||||
const trimmed = expanded.trim()
|
||||
tooltipHtml.set(
|
||||
node,
|
||||
trimmed
|
||||
? `<div class="markdown-body moderation-tooltip-markdown">${renderHighlightedString(trimmed)}</div>`
|
||||
: '',
|
||||
)
|
||||
} catch {
|
||||
tooltipHtml.set(node, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="flex ? 'flex flex-wrap gap-2' : 'space-y-4'">
|
||||
<template
|
||||
v-for="(item, idx) in nodes"
|
||||
:key="
|
||||
item instanceof NodeBuilder
|
||||
? item.type === 'button'
|
||||
? `button-${asButton(item).label}`
|
||||
: (asIdentified(item)._statePath?.join('/') ?? asIdentified(item).id ?? item.type)
|
||||
: typeof item === 'string'
|
||||
? `s-${item}`
|
||||
: `display-${idx}`
|
||||
"
|
||||
>
|
||||
<!-- Display items: plain strings or zero-arg render functions -->
|
||||
<template v-if="!(item instanceof NodeBuilder)">
|
||||
<template v-if="typeof item === 'string'">{{ item }}</template>
|
||||
<component :is="item" v-else />
|
||||
</template>
|
||||
|
||||
<template v-else-if="isVisible(item)">
|
||||
<div :class="item.type !== 'group' && !getNodeTitle(item) ? 'contents' : undefined">
|
||||
<div v-if="getNodeTitle(item)" class="mb-2" :class="titleClass(titleDepth ?? 0)">
|
||||
<span
|
||||
v-html="renderString(getNodeTitle(item)!).replace(/^<p>([\s\S]*)<\/p>\n?$/, '$1')"
|
||||
/><span v-if="nodeHasRequiredMissing(asIdentified(item))" class="text-red">*</span>
|
||||
</div>
|
||||
|
||||
<!-- group -->
|
||||
<template v-if="item.type === 'group'">
|
||||
<!-- multi-select (chips) mode -->
|
||||
<template v-if="asGroup(item)._selectMode === 'multi'">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template
|
||||
v-for="child in visibleChildren(asIdentified(item))"
|
||||
:key="asIdentified(child).id"
|
||||
>
|
||||
<ButtonStyled
|
||||
:color="
|
||||
getMultiSelectState(asIdentified(item)).has(asIdentified(child).id!)
|
||||
? hasActionableFixes(asIdentified(child))
|
||||
? 'blue'
|
||||
: 'brand'
|
||||
: 'standard'
|
||||
"
|
||||
:circular="!!asIdentified(child)._icon"
|
||||
@click="toggleChip(asIdentified(item), asIdentified(child))"
|
||||
>
|
||||
<button
|
||||
v-tooltip="getTooltipConfig(asIdentified(child))"
|
||||
:aria-label="asIdentified(child)._icon ? asLabeled(child).label : undefined"
|
||||
>
|
||||
<component :is="asIdentified(child)._icon" v-if="asIdentified(child)._icon" />
|
||||
<template v-else>{{ asLabeled(child).label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
<template
|
||||
v-for="child in visibleChildren(asIdentified(item))"
|
||||
:key="`sub-${asIdentified(child).id}`"
|
||||
>
|
||||
<NodeRenderer
|
||||
v-if="
|
||||
getMultiSelectState(asIdentified(item)).has(asIdentified(child).id!) &&
|
||||
getChildren(asIdentified(child)).length
|
||||
"
|
||||
:nodes="getChildren(asIdentified(child))"
|
||||
:show-context="getChildrenContext(asIdentified(child))"
|
||||
:on-image-upload="onImageUpload"
|
||||
:title-depth="titleDepth"
|
||||
:parent-state-path="asIdentified(child)._statePath ?? props.parentStatePath ?? []"
|
||||
class="mt-2"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<!-- single-select (button-style) mode -->
|
||||
<template v-else-if="asGroup(item)._selectMode === 'single'">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template
|
||||
v-for="child in visibleChildren(asIdentified(item))"
|
||||
:key="asIdentified(child).id"
|
||||
>
|
||||
<ButtonStyled
|
||||
:color="
|
||||
getSelectState(asIdentified(item)) === asIdentified(child).id
|
||||
? 'brand'
|
||||
: 'standard'
|
||||
"
|
||||
:circular="!!asIdentified(child)._icon"
|
||||
>
|
||||
<button
|
||||
:aria-label="asIdentified(child)._icon ? asLabeled(child).label : undefined"
|
||||
@click="toggleSelect(asIdentified(item), asIdentified(child))"
|
||||
>
|
||||
<component :is="asIdentified(child)._icon" v-if="asIdentified(child)._icon" />
|
||||
<template v-else>{{ asLabeled(child).label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
<template
|
||||
v-for="child in visibleChildren(asIdentified(item))"
|
||||
:key="`sub-${asIdentified(child).id}`"
|
||||
>
|
||||
<NodeRenderer
|
||||
v-if="
|
||||
getSelectState(asIdentified(item)) === asIdentified(child).id &&
|
||||
getChildren(asIdentified(child)).length
|
||||
"
|
||||
:nodes="getChildren(asIdentified(child))"
|
||||
:show-context="getChildrenContext(asIdentified(child))"
|
||||
:on-image-upload="onImageUpload"
|
||||
:title-depth="titleDepth"
|
||||
:parent-state-path="asIdentified(child)._statePath ?? props.parentStatePath ?? []"
|
||||
class="mt-2"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<!-- plain container mode -->
|
||||
<NodeRenderer
|
||||
v-else
|
||||
:nodes="getChildren(asIdentified(item))"
|
||||
:show-context="getChildrenContext(asIdentified(item))"
|
||||
:on-image-upload="onImageUpload"
|
||||
:flex="asGroup(item)._layout !== 'column'"
|
||||
:title-depth="item._title !== undefined ? (titleDepth ?? 0) + 1 : titleDepth"
|
||||
:parent-state-path="asIdentified(item)._statePath ?? props.parentStatePath ?? []"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- dropdown -->
|
||||
<template v-else-if="item.type === 'dropdown'">
|
||||
<Combobox
|
||||
class="!w-80"
|
||||
:options="getDropdownOptions(asDropdown(item))"
|
||||
:model-value="getDropdownModelValue(asDropdown(item))"
|
||||
trigger-class="!bg-[var(--color-button-bg)] !rounded-[var(--radius-md)] !shadow-[var(--shadow-inset-sm),0_0_0_0_transparent]"
|
||||
dropdown-class="!rounded-[var(--radius-md)] !bg-[var(--color-button-bg)] !border-0"
|
||||
@update:model-value="(v) => setNodeState(asIdentified(item), v || undefined)"
|
||||
/>
|
||||
<template
|
||||
v-for="child in visibleChildren(asIdentified(item))"
|
||||
:key="`sub-${asIdentified(child).id}`"
|
||||
>
|
||||
<NodeRenderer
|
||||
v-if="
|
||||
getSelectState(asIdentified(item)) === asIdentified(child).id &&
|
||||
getChildren(asIdentified(child)).length
|
||||
"
|
||||
:nodes="getChildren(asIdentified(child))"
|
||||
:show-context="getChildrenContext(asIdentified(child))"
|
||||
:on-image-upload="onImageUpload"
|
||||
:title-depth="titleDepth"
|
||||
:parent-state-path="asIdentified(child)._statePath ?? props.parentStatePath ?? []"
|
||||
class="mt-2"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- button -->
|
||||
<template v-else-if="item.type === 'button'">
|
||||
<ButtonStyled :circular="!!item._icon && !asButton(item).label">
|
||||
<button
|
||||
v-tooltip="getTooltipConfig(item, showContext)"
|
||||
:disabled="!isButtonEnabled(asButton(item))"
|
||||
:aria-label="item._icon && !asButton(item).label ? asButton(item).label : undefined"
|
||||
@click="handleButtonClick(asButton(item))"
|
||||
>
|
||||
<component :is="item._icon" v-if="item._icon" />
|
||||
{{ asButton(item).label }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
<!-- toggle -->
|
||||
<template v-else-if="item.type === 'toggle'">
|
||||
<ButtonStyled :color="getBooleanColor(asBool(item))" :circular="!!item._icon">
|
||||
<button
|
||||
v-tooltip="getTooltipConfig(asBool(item))"
|
||||
:disabled="!isEnabled(asIdentified(item))"
|
||||
:aria-label="item._icon ? asLabeled(item).label : undefined"
|
||||
@click="toggleBoolean(asBool(item))"
|
||||
>
|
||||
<component :is="item._icon" v-if="item._icon" />
|
||||
<template v-else>{{ asLabeled(item).label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
<!-- check -->
|
||||
<template v-else-if="item.type === 'check'">
|
||||
<Checkbox
|
||||
:model-value="getBooleanState(asBool(item))"
|
||||
:label="asLabeled(item).label"
|
||||
:disabled="!isEnabled(asIdentified(item))"
|
||||
@update:model-value="isEnabled(asIdentified(item)) && toggleBoolean(asBool(item))"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- text -->
|
||||
<template v-else-if="item.type === 'text'">
|
||||
<StyledInput
|
||||
:id="`node-${asIdentified(item).id}`"
|
||||
:ref="(el: any) => (el ? textInputRefs.set(item, el) : textInputRefs.delete(item))"
|
||||
v-tooltip="getTooltipConfig(item, showContext)"
|
||||
:model-value="getTextState(asIdentified(item))"
|
||||
:placeholder="getPlaceholder(asInput(item))"
|
||||
autocomplete="off"
|
||||
@update:model-value="(v: string) => setTextState(asIdentified(item), v)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- markdown -->
|
||||
<template v-else-if="item.type === 'markdown'">
|
||||
<MarkdownEditor
|
||||
:id="`node-${asIdentified(item).id}`"
|
||||
:aria-label="asLabeled(item).label || undefined"
|
||||
:model-value="getTextState(asIdentified(item))"
|
||||
:placeholder="getPlaceholder(asInput(item))"
|
||||
:max-height="300"
|
||||
:disabled="false"
|
||||
:heading-buttons="false"
|
||||
:on-image-upload="onImageUpload"
|
||||
@update:model-value="(v: string) => setTextState(asIdentified(item), v)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- children of active boolean nodes, rendered after all siblings -->
|
||||
<template
|
||||
v-for="(item, idx) in nodes"
|
||||
:key="
|
||||
item instanceof NodeBuilder
|
||||
? item.type === 'button'
|
||||
? `children-button-${asButton(item).label}`
|
||||
: `children-${asIdentified(item)._statePath?.join('/') ?? asIdentified(item).id ?? item.type}`
|
||||
: `children-display-${idx}`
|
||||
"
|
||||
>
|
||||
<NodeRenderer
|
||||
v-if="
|
||||
item instanceof NodeBuilder &&
|
||||
isVisible(item) &&
|
||||
(item.type === 'toggle' || item.type === 'check') &&
|
||||
getBooleanState(asBool(item)) &&
|
||||
getChildren(asIdentified(item)).length
|
||||
"
|
||||
:nodes="getChildren(asIdentified(item))"
|
||||
:show-context="getChildrenContext(asIdentified(item))"
|
||||
:on-image-upload="onImageUpload"
|
||||
:title-depth="item._title !== undefined ? (titleDepth ?? 0) + 1 : titleDepth"
|
||||
:parent-state-path="asIdentified(item)._statePath ?? props.parentStatePath ?? []"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { IdentifiedNodeBuilder, NodeState } from '@modrinth/moderation'
|
||||
import type { ComputedRef, InjectionKey, Ref } from 'vue'
|
||||
|
||||
export interface ActiveAction {
|
||||
node: IdentifiedNodeBuilder
|
||||
state: Record<string, NodeState>
|
||||
statePath: string[]
|
||||
}
|
||||
|
||||
export interface LiveNode {
|
||||
isActive: boolean
|
||||
isVisible: boolean
|
||||
isFixActionable: boolean
|
||||
messageCount: number
|
||||
fixCount: number
|
||||
hasRequiredMissing: boolean
|
||||
activeActions: ActiveAction[]
|
||||
}
|
||||
|
||||
export const NODE_META_KEY: InjectionKey<ComputedRef<Map<IdentifiedNodeBuilder, LiveNode>>> =
|
||||
Symbol('nodeMeta')
|
||||
export const STATE_KEY: InjectionKey<Ref<Record<string, Record<string, NodeState>>>> =
|
||||
Symbol('checklistState')
|
||||
@@ -129,6 +129,7 @@ provideModrinthClient(client)
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(false),
|
||||
showAds: ref(false),
|
||||
adConsentAvailable: ref(false),
|
||||
openExternalUrl: (url) => window.open(url, '_blank'),
|
||||
})
|
||||
|
||||
|
||||
@@ -443,20 +443,23 @@
|
||||
:options="[
|
||||
{
|
||||
id: 'new-project',
|
||||
action: (event) => $refs.modal_creation.show(event),
|
||||
action: (event) => requireVerifiedEmail(() => $refs.modal_creation.show(event)),
|
||||
},
|
||||
{
|
||||
id: 'new-server-project',
|
||||
action: (event) => $refs.modal_creation.show(event, { type: 'server' }),
|
||||
action: (event) =>
|
||||
requireVerifiedEmail(() => $refs.modal_creation.show(event, { type: 'server' })),
|
||||
},
|
||||
{
|
||||
id: 'new-collection',
|
||||
action: (event) => $refs.modal_collection_creation.show(event),
|
||||
action: (event) =>
|
||||
requireVerifiedEmail(() => $refs.modal_collection_creation.show(event)),
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
id: 'new-organization',
|
||||
action: (event) => $refs.modal_organization_creation.show(event),
|
||||
action: (event) =>
|
||||
requireVerifiedEmail(() => $refs.modal_organization_creation.show(event)),
|
||||
},
|
||||
]"
|
||||
>
|
||||
@@ -777,6 +780,7 @@ import {
|
||||
createHostingIntercomIdentityKey,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
OverflowMenu,
|
||||
providePageContext,
|
||||
@@ -814,6 +818,7 @@ const generatedState = useGeneratedState()
|
||||
const country = useUserCountry()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const auth = await useAuth()
|
||||
const user = await useUser()
|
||||
@@ -906,6 +911,19 @@ async function fetchIntercomToken() {
|
||||
})
|
||||
}
|
||||
|
||||
function requireVerifiedEmail(action) {
|
||||
if (!auth.value.user?.email_verified) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.emailVerificationRequired),
|
||||
text: formatMessage(messages.verifyEmailBeforePublishing),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
action()
|
||||
}
|
||||
|
||||
const navMenuMessages = defineMessages({
|
||||
home: {
|
||||
id: 'layout.nav.home',
|
||||
@@ -962,6 +980,14 @@ const messages = defineMessages({
|
||||
id: 'layout.action.publish',
|
||||
defaultMessage: 'Publish',
|
||||
},
|
||||
emailVerificationRequired: {
|
||||
id: 'layout.publish.email-verification-required.title',
|
||||
defaultMessage: 'Email verification required',
|
||||
},
|
||||
verifyEmailBeforePublishing: {
|
||||
id: 'layout.publish.email-verification-required.description',
|
||||
defaultMessage: 'You must verify your email before publishing on Modrinth.',
|
||||
},
|
||||
reviewProjects: {
|
||||
id: 'layout.action.review-projects',
|
||||
defaultMessage: 'Project review',
|
||||
|
||||
@@ -2171,9 +2171,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Kolekce"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {stažení} few {stažení} other {stažení}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Členem od"
|
||||
},
|
||||
@@ -2192,9 +2189,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizace"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {projekt} few {projekty} other {projektů}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Ukládání..."
|
||||
},
|
||||
@@ -2528,12 +2522,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Přejít na přehled projektů"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {stažení} few {stažení} other {stažení}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {# sledující} few {# sledující} other {# sledujících}}"
|
||||
},
|
||||
"project.versions.title": {
|
||||
"message": "Verze"
|
||||
},
|
||||
|
||||
@@ -2999,9 +2999,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Überprüfe deine Wallet-Adresse. An eine falsche Adresse gesendete Gelder können nicht wiederhergestellt werden."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Die Änderungen werden auf {count, plural, one {# Projekt} other {# Projekte}} angewendet."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Ein Modrinth Ersteller."
|
||||
},
|
||||
@@ -3068,9 +3065,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Kollektion"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {Download} other {Downloads}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Beigetreten"
|
||||
},
|
||||
@@ -3089,9 +3083,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organisationen"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {Projekt} other {Projekte}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Speichert..."
|
||||
},
|
||||
@@ -3656,12 +3647,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Projekt-Dashboard besuchen"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {Download} other {Downloads}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {Follower} other {Follower}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheided sich, das Projekt zu Dearchivieren."
|
||||
},
|
||||
|
||||
@@ -2999,9 +2999,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Überprüfe deine Wallet-Adresse. An eine falsche Adresse gesendete Gelder können nicht wiederhergestellt werden."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Änderungen werden auf {count, plural, one {# Projekt} other {# Projekte}} angewendet."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Ein Modrinth-Ersteller."
|
||||
},
|
||||
@@ -3068,9 +3065,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Kollektion"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {Download} other {Downloads}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Beigetreten"
|
||||
},
|
||||
@@ -3089,9 +3083,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organisationen"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {Projekt} other {Projekte}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Speichert..."
|
||||
},
|
||||
@@ -3656,12 +3647,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Projekt-Dashboard besuchen"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {Download} other {Downloads}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {Follower} other {Follower}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheidet sich das Projekt zu dearchivieren."
|
||||
},
|
||||
|
||||
@@ -8,6 +8,21 @@
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "User not found"
|
||||
},
|
||||
"ads-consent.accept": {
|
||||
"message": "Accept all"
|
||||
},
|
||||
"ads-consent.body": {
|
||||
"message": "Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies on the website to personalize ads and measure performance."
|
||||
},
|
||||
"ads-consent.manage": {
|
||||
"message": "Manage preferences"
|
||||
},
|
||||
"ads-consent.reject": {
|
||||
"message": "Reject all"
|
||||
},
|
||||
"ads-consent.title": {
|
||||
"message": "Your privacy and how ads support Modrinth"
|
||||
},
|
||||
"analytics.action.add": {
|
||||
"message": "Add"
|
||||
},
|
||||
@@ -2672,6 +2687,12 @@
|
||||
"layout.nav.upgrade-to-modrinth-plus": {
|
||||
"message": "Upgrade to Modrinth+"
|
||||
},
|
||||
"layout.publish.email-verification-required.description": {
|
||||
"message": "You must verify your email before publishing on Modrinth."
|
||||
},
|
||||
"layout.publish.email-verification-required.title": {
|
||||
"message": "Email verification required"
|
||||
},
|
||||
"moderation.exclude-technical-review": {
|
||||
"message": "Exclude TR"
|
||||
},
|
||||
@@ -2693,6 +2714,9 @@
|
||||
"moderation.page.technicalReview": {
|
||||
"message": "Tech review"
|
||||
},
|
||||
"modpack-scan-modal.clear-all-groups": {
|
||||
"message": "Clear All Groups"
|
||||
},
|
||||
"modpack-scan-modal.failed": {
|
||||
"message": "Failed"
|
||||
},
|
||||
@@ -2999,8 +3023,23 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Double-check your wallet address. Funds sent to an incorrect address cannot be recovered."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Changes will be applied to {count, plural, one {# project} other {# projects}}."
|
||||
"organization.button.manage": {
|
||||
"message": "Manage"
|
||||
},
|
||||
"organization.button.manage-projects": {
|
||||
"message": "Manage projects"
|
||||
},
|
||||
"organization.label.downloads": {
|
||||
"message": "downloads"
|
||||
},
|
||||
"organization.label.members": {
|
||||
"message": "members"
|
||||
},
|
||||
"organization.label.organization": {
|
||||
"message": "Organization"
|
||||
},
|
||||
"organization.label.projects": {
|
||||
"message": "projects"
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "A Modrinth creator."
|
||||
@@ -3068,8 +3107,8 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Collection"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
|
||||
"profile.label.download-count": {
|
||||
"message": "{count, plural, one {download} other {downloads}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Joined"
|
||||
@@ -3089,8 +3128,8 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizations"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {project} other {projects}}"
|
||||
"profile.label.project-count": {
|
||||
"message": "{count, plural, one {project} other {projects}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Saving..."
|
||||
@@ -3254,6 +3293,12 @@
|
||||
"project.actions.dont-show-again": {
|
||||
"message": "Don't show again"
|
||||
},
|
||||
"project.actions.edit-project": {
|
||||
"message": "Edit project"
|
||||
},
|
||||
"project.actions.rescan-modpack": {
|
||||
"message": "Rescan modpack"
|
||||
},
|
||||
"project.actions.review-project": {
|
||||
"message": "Review project"
|
||||
},
|
||||
@@ -3656,12 +3701,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Visit projects dashboard"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {download} other {downloads}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {follower} other {followers}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project."
|
||||
},
|
||||
|
||||
@@ -2999,9 +2999,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Verifica dos veces la dirección de tu billetera. Los fondos enviados a una dirección incorrecta no se pueden recuperar."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Estos cambios serán aplicados {count, plural, one {al proyecto} other {a los # proyectos}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Un creador de Modrinth."
|
||||
},
|
||||
@@ -3068,9 +3065,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Colección"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {descarga} other {descargas}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Se unió"
|
||||
},
|
||||
@@ -3089,9 +3083,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizaciones"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {proyecto} other {proyectos}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Guardando..."
|
||||
},
|
||||
@@ -3656,12 +3647,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Ver el panel de control de proyectos"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {descarga} other {descargas}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {seguidor} other {seguidores}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} se ha archivado. {title} no recibirá más actualizaciones hasta que el autor decida desarchivar el proyecto."
|
||||
},
|
||||
|
||||
@@ -2822,9 +2822,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Verifique la dirección de su billetera. Los fondos enviados a una dirección incorrecta no se pueden recuperar."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Estos cambios serán aplicados {count, plural, one {al proyecto} other {a los # proyectos}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Creador en Modrinth."
|
||||
},
|
||||
@@ -2891,9 +2888,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Colección"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {descarga} other {descargas}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Se unió hace"
|
||||
},
|
||||
@@ -2912,9 +2906,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizaciones"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {proyecto} other {proyectos}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Guardando..."
|
||||
},
|
||||
@@ -3377,12 +3368,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Visita el panel de proyectos"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {descarga} other {descargas}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {seguidor} other {seguidores}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} ha sido archivado. {title} no recibirá ninguna futura actualización excepto que el autor decida desarchivar el proyecto."
|
||||
},
|
||||
|
||||
@@ -2213,9 +2213,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Koleksiyon"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {download} other {na download}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Sumali"
|
||||
},
|
||||
@@ -2234,9 +2231,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Mga organisasyon"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {proyekto} other {na proyekto}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Sine-save..."
|
||||
},
|
||||
|
||||
@@ -2472,7 +2472,7 @@
|
||||
"message": "Ce déploiement du frontend de Modrinth n’a pas pu générer l’état à partir de l’API. Cela peut être dû à une panne ou à une erreur de configuration. Rebuild lorsque l’API sera disponible. Codes d’erreur : {errors} ; L’URL actuelle de l’API est : {url}"
|
||||
},
|
||||
"layout.banner.build-fail.ignore": {
|
||||
"message": "ignorer"
|
||||
"message": "Ignorer"
|
||||
},
|
||||
"layout.banner.build-fail.title": {
|
||||
"message": "Erreur lors de la génération de l’état à partir de l’API pendant la construction."
|
||||
@@ -2672,6 +2672,12 @@
|
||||
"layout.nav.upgrade-to-modrinth-plus": {
|
||||
"message": "Passer à Modrinth+"
|
||||
},
|
||||
"layout.publish.email-verification-required.description": {
|
||||
"message": "Vous devez vérifier votre adresse e-mail avant de publier sur Modrinth."
|
||||
},
|
||||
"layout.publish.email-verification-required.title": {
|
||||
"message": "Vérification de l'adresse e-mail requise"
|
||||
},
|
||||
"moderation.exclude-technical-review": {
|
||||
"message": "Exclure la révision technique"
|
||||
},
|
||||
@@ -2999,9 +3005,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Vérifiez votre adresse. Les fonds envoyés à une adresse incorrecte ne pourront pas être récupérés."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Les modifications seront appliquées à {count, plural, one {# projet} other {# projets}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Un créateur Modrinth."
|
||||
},
|
||||
@@ -3068,9 +3071,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Collection"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {téléchargement} other {téléchargements}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Rejoint"
|
||||
},
|
||||
@@ -3089,9 +3089,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organisations"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {projet} other {projets}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Sauvegarde en cours..."
|
||||
},
|
||||
@@ -3395,6 +3392,12 @@
|
||||
"project.download.unknown-loader": {
|
||||
"message": "Loader inconnu"
|
||||
},
|
||||
"project.download.zip-failed-text": {
|
||||
"message": "Un ou plusieurs fichiers n'ont pas pu être téléchargés. Veuillez essayer à nouveau."
|
||||
},
|
||||
"project.download.zip-failed-title": {
|
||||
"message": "Impossible de télécharger les fichiers"
|
||||
},
|
||||
"project.environment.migration-no-permission.message": {
|
||||
"message": "Nous venons de remanier le système d'Environnements sur Modrinth et de nouvelles options sont désormais disponibles. Vous n'avez pas l'autorisation de modifier ces paramètres, veuillez informer un autre membre du projet que les métadonnées de l'environnement doivent être vérifiées."
|
||||
},
|
||||
@@ -3426,7 +3429,7 @@
|
||||
"message": "Galerie"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Le texte de la license n'a pas pu être obtenu"
|
||||
"message": "Le texte de la license n'a pas pu être obtenu."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Chargement du texte de la license..."
|
||||
@@ -3528,7 +3531,7 @@
|
||||
"message": "Projet mis à jour"
|
||||
},
|
||||
"project.settings.general.name.description": {
|
||||
"message": "Évitez les préfixes, suffixes, parenthèses, ou les descriptions - juste le nom actuel du projet"
|
||||
"message": "Évitez les préfixes, suffixes, parenthèses, ou les descriptions - juste le nom actuel du projet."
|
||||
},
|
||||
"project.settings.general.name.placeholder.1": {
|
||||
"message": "ex. Nether Overhaul 2"
|
||||
@@ -3650,12 +3653,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Visiter le tableau de contrôle des projets"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {# téléchargement} other {# téléchargements}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {# suivi} other {# suivis}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} a été archivé. {title} ne recevra plus de mises à jour jusqu'à ce que l'auteur du projet décide de désarchiver le projet."
|
||||
},
|
||||
@@ -3681,7 +3678,7 @@
|
||||
"message": "Incluez des liens et des images si possible et pertinentes. Les signalements vides ou insuffisants seront fermés et ignorés."
|
||||
},
|
||||
"report.body.title": {
|
||||
"message": "Veuillez fournir des informations supplémentaires concernant votre signalement."
|
||||
"message": "Veuillez fournir des informations supplémentaires concernant votre signalement"
|
||||
},
|
||||
"report.checking": {
|
||||
"message": "Vérification de {item}..."
|
||||
|
||||
@@ -8,6 +8,21 @@
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "משתמש לא קיים"
|
||||
},
|
||||
"analytics.action.add": {
|
||||
"message": "לְהוֹסִיף"
|
||||
},
|
||||
"analytics.action.cancel": {
|
||||
"message": "ביטול"
|
||||
},
|
||||
"analytics.breakdown.project": {
|
||||
"message": "פּרוֹיֶקט"
|
||||
},
|
||||
"analytics.chart.legend.monetization-details.description": {
|
||||
"message": "רק צפיות והורדות שבוצעו דרך Modrinth נספרות לצורך מונטיזציה, והורדות מחייבות את המשתמשים להיות מחוברים לחשבונם."
|
||||
},
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "להציג את כל {count} הקווים בגרף?"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "היישום השולחני שלנו זמין לכל הפלטפורמות. אנא בחר את הגרסה הרצויה."
|
||||
},
|
||||
@@ -1823,9 +1838,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "אוסף"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "הצטרף ב-"
|
||||
},
|
||||
@@ -1844,9 +1856,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "ארגונים"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {project} other {projects}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "שומר..."
|
||||
},
|
||||
|
||||
@@ -84,13 +84,13 @@
|
||||
"message": "Megjegyzések"
|
||||
},
|
||||
"analytics.chart.controls.aria": {
|
||||
"message": "Elemzési grafikonvezérlők, {activeCount}"
|
||||
"message": "Statisztikai grafikonvezérlők, {activeCount}"
|
||||
},
|
||||
"analytics.chart.controls.button": {
|
||||
"message": "Irányítások"
|
||||
"message": "Vezérlők"
|
||||
},
|
||||
"analytics.chart.controls.dialog-aria": {
|
||||
"message": "Elemzési grafikonok vezérlőelemei"
|
||||
"message": "Statisztikai grafikonok vezérlőelemei"
|
||||
},
|
||||
"analytics.chart.controls.display": {
|
||||
"message": "Kijelző"
|
||||
@@ -159,7 +159,7 @@
|
||||
"message": "{project}-tól/től függ"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {#nap} other {#napok}}"
|
||||
"message": "{count} nap"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.hours": {
|
||||
"message": "{count, plural, one {#óra} other {# órák}}"
|
||||
@@ -270,10 +270,10 @@
|
||||
"message": "Megtekintések alakulása"
|
||||
},
|
||||
"analytics.group-by.1h": {
|
||||
"message": "1ó"
|
||||
"message": "1 ó"
|
||||
},
|
||||
"analytics.group-by.6h": {
|
||||
"message": "6ó"
|
||||
"message": "6 ó"
|
||||
},
|
||||
"analytics.group-by.date": {
|
||||
"message": "Dátum"
|
||||
@@ -381,7 +381,7 @@
|
||||
"message": "Lebontás:"
|
||||
},
|
||||
"analytics.query.label.grouped-by": {
|
||||
"message": "Csoportosítás"
|
||||
"message": "Csoportosítás:"
|
||||
},
|
||||
"analytics.query.label.project": {
|
||||
"message": "Projekt:"
|
||||
@@ -408,7 +408,7 @@
|
||||
"message": "{hours} óra"
|
||||
},
|
||||
"analytics.stat.previous-period-comparison": {
|
||||
"message": "az előző időszakhoz képes"
|
||||
"message": "az előző időszakhoz képest"
|
||||
},
|
||||
"analytics.stat.previous-period-comparison-short": {
|
||||
"message": "az előzőhöz képest"
|
||||
@@ -438,7 +438,7 @@
|
||||
"message": "Kiválasztott hossz"
|
||||
},
|
||||
"analytics.table.duration.days": {
|
||||
"message": "{count, plural, one {# day} other {# days}}"
|
||||
"message": "{count} nap"
|
||||
},
|
||||
"analytics.table.duration.hours": {
|
||||
"message": "{count, plural, one {# hour} other {# hours}}"
|
||||
@@ -2039,6 +2039,9 @@
|
||||
"hosting-marketing.faq.location": {
|
||||
"message": "Hol találhatók a Modrinth Hosting szerverei? Kiválaszthatom a régiót?"
|
||||
},
|
||||
"hosting-marketing.faq.location.answer": {
|
||||
"message": "Jelenleg Észak-Amerikában, Európában, Délkelet-Ázsiában és Ausztráliában rendelkezünk szerverekkel, amelyeket vásárláskor kiválaszthatsz. A jövőben további régiók érkeznek! Ha régiót szeretnél váltani, kérlek vedd fel a kapcsolatot az ügyfélszolgálattal."
|
||||
},
|
||||
"hosting-marketing.faq.versions-loaders": {
|
||||
"message": "Melyik Minecraft verziók és betöltők használhatók?"
|
||||
},
|
||||
@@ -2894,9 +2897,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Nézd át jól a pénztárca címedet. Az egyenleg rossz címre lesz küldve az nem szerezhető vissza."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "A módosítások {count, plural, one {# projktre} other {# projektekre}} fognak vonatkozni."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Egy Modrinth fejlesztő."
|
||||
},
|
||||
@@ -2963,9 +2963,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Gyűjtemény"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {letöltés} other {letöltések}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Csatlakozott"
|
||||
},
|
||||
@@ -2984,9 +2981,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Szervezetek"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {projekt} other {projektek}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Mentés..."
|
||||
},
|
||||
@@ -3440,6 +3434,9 @@
|
||||
"project.settings.permissions.completed.description": {
|
||||
"message": "Minden külső tartalomhoz megadva vannak a felhasználási engedélyek és a forrásmegjelölések."
|
||||
},
|
||||
"project.settings.permissions.completed.title": {
|
||||
"message": "Engedélyek beállítva!"
|
||||
},
|
||||
"project.settings.permissions.empty-state.heading": {
|
||||
"message": "Kész is van!"
|
||||
},
|
||||
@@ -3452,18 +3449,24 @@
|
||||
"project.settings.permissions.search-placeholder": {
|
||||
"message": "Keresés {count} projekt között..."
|
||||
},
|
||||
"project.settings.permissions.sort.most-files": {
|
||||
"message": "Legtöbb fájl"
|
||||
},
|
||||
"project.settings.permissions.sort.recently-edited": {
|
||||
"message": "Nemrég szerkesztett"
|
||||
},
|
||||
"project.settings.permissions.sort.rejected": {
|
||||
"message": "Elutasítva"
|
||||
},
|
||||
"project.settings.permissions.sort.status": {
|
||||
"message": "Állapot"
|
||||
},
|
||||
"project.settings.title": {
|
||||
"message": "Beállítások"
|
||||
},
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Projektek irányítópult megnyitása"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count} letöltés"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count} követő"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "A(z) {title} archiválásra került. A(z) {title} nem kap további frissítéseket, kivéve, ha a fejlesztő úgy dönt, hogy visszavonja a projekt archiválását."
|
||||
},
|
||||
@@ -3977,6 +3980,9 @@
|
||||
"settings.account.security.email.title": {
|
||||
"message": "E-mail cím"
|
||||
},
|
||||
"settings.account.security.passkey.modal.added": {
|
||||
"message": "Hozzá adva {ago}"
|
||||
},
|
||||
"settings.account.security.password.action.add": {
|
||||
"message": "Jelszó hozzáadása"
|
||||
},
|
||||
@@ -4580,6 +4586,9 @@
|
||||
"ui.newsletter-button.tooltip": {
|
||||
"message": "Feliratkozás a Modrinth hírlevelére"
|
||||
},
|
||||
"version.confirm-delete.title": {
|
||||
"message": "Biztos, hogy le akarod törölni ezt a verziót?"
|
||||
},
|
||||
"version.download.no-primary-file": {
|
||||
"message": "Hiba: Nem található az elsődleges fájl"
|
||||
},
|
||||
@@ -4594,5 +4603,14 @@
|
||||
},
|
||||
"version.environment.unknown.title": {
|
||||
"message": "Ismeretlen környezet"
|
||||
},
|
||||
"version.section.content.dev-info.gradle-snippet": {
|
||||
"message": "build.gradle:"
|
||||
},
|
||||
"version.supplementary-resources.copy-hash-sha1": {
|
||||
"message": "SHA-1 Másolása"
|
||||
},
|
||||
"version.supplementary-resources.copy-hash-sha512": {
|
||||
"message": "SHA-512 Másolása"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2219,9 +2219,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Koleksi"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, other {pengunduhan}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Telah bergabung"
|
||||
},
|
||||
@@ -2240,9 +2237,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organisasi"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, other {proyek}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Menyimpan..."
|
||||
},
|
||||
|
||||
@@ -2990,9 +2990,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Controlla nuovamente il tuo conto. Fondi mandati a un conto errato non possono essere recuperati."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Le modifiche verranno applicate a {count, plural, one {# progetto} other {# progetti}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Creatore su Modrinth."
|
||||
},
|
||||
@@ -3059,9 +3056,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Raccolta"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} download"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Iscrizione"
|
||||
},
|
||||
@@ -3080,9 +3074,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizzazioni"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {progetto} other {progetti}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Salvando..."
|
||||
},
|
||||
@@ -3647,12 +3638,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Visita bacheca del progetto"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count} download"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count} follower"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} è stato archiviato. {title} non riceverà più aggiornamenti a meno che l'autore decida di rimuoverlo dall'archivio."
|
||||
},
|
||||
|
||||
@@ -21,19 +21,19 @@
|
||||
"message": "リセット"
|
||||
},
|
||||
"analytics.breakdown.country": {
|
||||
"message": "国"
|
||||
"message": "国 / 地域"
|
||||
},
|
||||
"analytics.breakdown.dependent-project-download": {
|
||||
"message": "依存プロジェクト"
|
||||
},
|
||||
"analytics.breakdown.dependent-project-type": {
|
||||
"message": "依存プロジェクトの種類"
|
||||
"message": "依存プロジェクトのタイプ"
|
||||
},
|
||||
"analytics.breakdown.download-reason": {
|
||||
"message": "きっかけ"
|
||||
"message": "ダウンロードの追加情報"
|
||||
},
|
||||
"analytics.breakdown.download-source": {
|
||||
"message": "ソース"
|
||||
"message": "プラットフォーム"
|
||||
},
|
||||
"analytics.breakdown.game-version": {
|
||||
"message": "ゲームのバージョン"
|
||||
@@ -42,13 +42,13 @@
|
||||
"message": "内訳"
|
||||
},
|
||||
"analytics.breakdown.loader": {
|
||||
"message": "ローダー"
|
||||
"message": "ゲームのローダー"
|
||||
},
|
||||
"analytics.breakdown.members": {
|
||||
"message": "メンバー"
|
||||
},
|
||||
"analytics.breakdown.monetization": {
|
||||
"message": "収益"
|
||||
"message": "マネタイズ"
|
||||
},
|
||||
"analytics.breakdown.none.selected": {
|
||||
"message": "分析 - デフォルト"
|
||||
@@ -158,6 +158,9 @@
|
||||
"analytics.empty.no-data-for-analytics": {
|
||||
"message": "統計に使用可能なデータがありません"
|
||||
},
|
||||
"analytics.filter.game-version-type": {
|
||||
"message": "ゲームバージョンタイプ"
|
||||
},
|
||||
"analytics.filter.game-version-type.all": {
|
||||
"message": "全て"
|
||||
},
|
||||
@@ -245,6 +248,12 @@
|
||||
"analytics.project-status.approved": {
|
||||
"message": "承認済み"
|
||||
},
|
||||
"analytics.project-status.draft": {
|
||||
"message": "ドラフト"
|
||||
},
|
||||
"analytics.project-status.other": {
|
||||
"message": "その他"
|
||||
},
|
||||
"analytics.project-status.private": {
|
||||
"message": "非公開"
|
||||
},
|
||||
@@ -254,6 +263,12 @@
|
||||
"analytics.project.all": {
|
||||
"message": "全てのプロジェクト"
|
||||
},
|
||||
"analytics.project.select": {
|
||||
"message": "プロジェクトを選択"
|
||||
},
|
||||
"analytics.project.your": {
|
||||
"message": "あなたのプロジェクト"
|
||||
},
|
||||
"analytics.query.filter.add": {
|
||||
"message": "条件を追加"
|
||||
},
|
||||
@@ -336,7 +351,7 @@
|
||||
"message": "アナリティクス"
|
||||
},
|
||||
"analytics.value.monetized": {
|
||||
"message": "収益対象"
|
||||
"message": "収益の対象"
|
||||
},
|
||||
"analytics.value.no-dependent": {
|
||||
"message": "なし"
|
||||
@@ -351,7 +366,7 @@
|
||||
"message": "不明"
|
||||
},
|
||||
"analytics.value.unmonetized": {
|
||||
"message": "収益対象外"
|
||||
"message": "収益の対象外"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "我々のデスクトップアプリはすべてのプラットフォームでご利用いただけますので、ご希望のバージョンをお選びください。"
|
||||
@@ -2582,9 +2597,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "コレクション"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count}件のダウンロード"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "参加: "
|
||||
},
|
||||
@@ -2603,9 +2615,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "組織"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {プロジェクト} other {プロジェクト}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "保存中…"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2684,9 +2684,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Semak semula alamat dompet anda. Dana yang dihantar ke alamat yang salah tidak boleh dipulihkan."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Pemasangan akan diterapkan kepada {count, plural, other {# projek}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Seorang pencipta Modrinth."
|
||||
},
|
||||
@@ -2750,9 +2747,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Koleksi"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, other {muat turun}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Telah menyertai"
|
||||
},
|
||||
@@ -2771,9 +2765,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organisasi"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, other {projek}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Sedang menyimpan..."
|
||||
},
|
||||
@@ -3188,9 +3179,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Kunjungi papan pemuka projek"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, other {muat turun}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} telah diarkibkan. {title} tidak akan menerima sebarang kemas kini lanjut melainkan pengarang memutuskan untuk menyaharkibkan projek."
|
||||
},
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
"analytics.breakdown.game-version": {
|
||||
"message": "Spel versie"
|
||||
},
|
||||
"analytics.breakdown.generic": {
|
||||
"message": "Uitsplitsing"
|
||||
},
|
||||
"analytics.breakdown.loader": {
|
||||
"message": "Lader"
|
||||
},
|
||||
@@ -50,6 +53,9 @@
|
||||
"analytics.breakdown.monetization": {
|
||||
"message": "Monetisatie"
|
||||
},
|
||||
"analytics.breakdown.none.selected": {
|
||||
"message": "Geen uitsplitsing"
|
||||
},
|
||||
"analytics.breakdown.project": {
|
||||
"message": "Project"
|
||||
},
|
||||
@@ -59,9 +65,15 @@
|
||||
"analytics.breakdown.project-version": {
|
||||
"message": "Project versie"
|
||||
},
|
||||
"analytics.breakdown.selected": {
|
||||
"message": "Uitsplitsing naar {breakdown}"
|
||||
},
|
||||
"analytics.chart.action.show-all": {
|
||||
"message": "Allemaal laten zien"
|
||||
},
|
||||
"analytics.chart.action.show-limited": {
|
||||
"message": "Beperkt weergeven"
|
||||
},
|
||||
"analytics.chart.action.show-top-eight": {
|
||||
"message": "Laat de top 8 zien"
|
||||
},
|
||||
@@ -74,6 +86,15 @@
|
||||
"analytics.chart.controls.annotations": {
|
||||
"message": "Annotatie"
|
||||
},
|
||||
"analytics.chart.controls.aria": {
|
||||
"message": "Statistiekinstellingen, {activeCount}"
|
||||
},
|
||||
"analytics.chart.controls.button": {
|
||||
"message": "Instellingen"
|
||||
},
|
||||
"analytics.chart.controls.dialog-aria": {
|
||||
"message": "Statistiekinstellingen"
|
||||
},
|
||||
"analytics.chart.controls.display": {
|
||||
"message": "Weergave"
|
||||
},
|
||||
@@ -98,18 +119,51 @@
|
||||
"analytics.chart.empty.select-table-items": {
|
||||
"message": "Selecteer een element uit de tabel beneden om jouw data te visualiseren."
|
||||
},
|
||||
"analytics.chart.events.count-aria": {
|
||||
"message": "{count, plural, one {# statistiekgebeurtenis} other {# statistiekgebeurtenissen}}"
|
||||
},
|
||||
"analytics.chart.events.project-title": {
|
||||
"message": "<project>{projectName}</project>: {title}"
|
||||
},
|
||||
"analytics.chart.events.see-announcement": {
|
||||
"message": "Zie aankondiging"
|
||||
},
|
||||
"analytics.chart.legend.monetization-details.aria": {
|
||||
"message": "Details van statistieken over inkomsten bekijken"
|
||||
},
|
||||
"analytics.chart.legend.monetization-details.description": {
|
||||
"message": "Alleen weergaven en downloads via Modrinth tellen mee voor het genereren van inkomsten, en voor downloads moeten gebruikers ingelogd zijn."
|
||||
},
|
||||
"analytics.chart.legend.monetization-details.title": {
|
||||
"message": "Details over genereren van inkomsten"
|
||||
},
|
||||
"analytics.chart.legend.previous-period-suffix": {
|
||||
"message": "{name} (vorige)"
|
||||
},
|
||||
"analytics.chart.render-limit.description": {
|
||||
"message": "Het laten zien van alle lijnen in de grafiek kan de pagina slomer maken."
|
||||
},
|
||||
"analytics.chart.render-limit.header": {
|
||||
"message": "Alle {count} punten in de grafiek laten zien?"
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Alle weergegeven {itemType, select, project {{count, plural, one {project} other {projecten}}} country {{count, plural, one {land} other {landen}}} monetization {{count, plural, one {inkomstenwaarde} other {inkomstenwaarden}}} downloadSource {{count, plural, one {downloadbron} other {downloadbronnen}}} downloadReason {{count, plural, one {downloadreden} other {downloadredenen}}} member {{count, plural, one {lid} other {leden}}} projectVersion {{count, plural, one {projectversie} other {projectversies}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {spelversie} other {spelversies}}} other {{count, plural, one {item} other {items}}}} uit tabel"
|
||||
},
|
||||
"analytics.chart.table-selection.count": {
|
||||
"message": "{count} {itemType, select, project {{count, plural, one {project} other {projecten}}} country {{count, plural, one {land} other {landen}}} monetization {{count, plural, one {inkomstenwaarde} other {inkomstenwaardes}}} downloadSource {{count, plural, one {downloadbron} other {downloadbronnen}}} downloadReason {{count, plural, one {downloadreden} other {downloadredenen}}} member {{count, plural, one {lid} other {leden}}} projectVersion {{count, plural, one {projectversie} other {projectversies}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {spelversie} other {spelversies}}} other {{count, plural, one {item} other {items}}}} uit tabel weergegeven"
|
||||
},
|
||||
"analytics.chart.table-selection.limited": {
|
||||
"message": "{limit} {itemType, select, project {{limit, plural, one {project} other {projecten}}} country {{limit, plural, one {land} other {landen}}} monetization {{limit, plural, one {inkomstenwaarde} other {inkomstenwaardes}}} downloadSource {{limit, plural, one {downloadbron} other {downloadbronnen}}} downloadReason {{limit, plural, one {downloadreden} other {downloadredenen}}} member {{limit, plural, one {lid} other {leden}}} projectVersion {{limit, plural, one {projectversie} other {projectversies}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {spelversie} other {spelversies}}} other {{limit, plural, one {item} other {items}}}} uit tabel weergegeven"
|
||||
},
|
||||
"analytics.chart.table-selection.top": {
|
||||
"message": "Top {count} {itemType, select, project {{count, plural, one {project} other {projecten}}} country {{count, plural, one {land} other {landen}}} monetization {{count, plural, one {inkomstenwaarde} other {inkomstenwaardes}}} downloadSource {{count, plural, one {downloadbron} other {downloadbronnen}}} downloadReason {{count, plural, one {downloadreden} other {downloadredenen}}} member {{count, plural, one {lid} other {leden}}} projectVersion {{count, plural, one {projectversie} other {projectversies}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {spelversie} other {spelversies}}} other {{count, plural, one {item} other {items}}}} uit tabel"
|
||||
},
|
||||
"analytics.chart.tooltip.dependent-on-project": {
|
||||
"message": "Afhankelijk van {project}"
|
||||
},
|
||||
"analytics.chart.tooltip.dependent-project-version": {
|
||||
"message": "{dependentProject} is afhankelijk van {dependencyProject}, {version}"
|
||||
},
|
||||
"analytics.chart.tooltip.duration.days": {
|
||||
"message": "{count, plural, one {# dag} other {# dagen}}"
|
||||
},
|
||||
@@ -128,6 +182,9 @@
|
||||
"analytics.chart.tooltip.pinned-aria": {
|
||||
"message": "Vastgepind"
|
||||
},
|
||||
"analytics.chart.tooltip.previous-period-short": {
|
||||
"message": "(vorige)"
|
||||
},
|
||||
"analytics.chart.tooltip.show-entry": {
|
||||
"message": "Laat {name} in de grafiek zien"
|
||||
},
|
||||
@@ -143,6 +200,9 @@
|
||||
"analytics.chart.view.line": {
|
||||
"message": "Lijn"
|
||||
},
|
||||
"analytics.download-reason.dependency": {
|
||||
"message": "Afhankelijkheid"
|
||||
},
|
||||
"analytics.download-reason.modpack": {
|
||||
"message": "Modpack"
|
||||
},
|
||||
@@ -173,6 +233,9 @@
|
||||
"analytics.empty.no-projects-for-analytics": {
|
||||
"message": "Geen projecten beschikbaar voor analyse"
|
||||
},
|
||||
"analytics.empty.select-project": {
|
||||
"message": "Selecteer ten minste één project om de gegevens te bekijken"
|
||||
},
|
||||
"analytics.filter.game-version-type": {
|
||||
"message": "Game versie typen"
|
||||
},
|
||||
@@ -185,9 +248,15 @@
|
||||
"analytics.filter.search.countries": {
|
||||
"message": "Zoek landen..."
|
||||
},
|
||||
"analytics.filter.search.dependent-projects": {
|
||||
"message": "Projecten zoeken..."
|
||||
},
|
||||
"analytics.filter.search.download-sources": {
|
||||
"message": "Zoek download bron..."
|
||||
},
|
||||
"analytics.filter.search.members": {
|
||||
"message": "Leden zoeken..."
|
||||
},
|
||||
"analytics.filter.search.project-versions": {
|
||||
"message": "Zoek project versies..."
|
||||
},
|
||||
@@ -203,6 +272,9 @@
|
||||
"analytics.graph.title.revenue": {
|
||||
"message": "Inkomsten Over Tijd"
|
||||
},
|
||||
"analytics.graph.title.views": {
|
||||
"message": "Ontwikkeling van aantal weergaven"
|
||||
},
|
||||
"analytics.group-by.1h": {
|
||||
"message": "1h"
|
||||
},
|
||||
@@ -287,6 +359,9 @@
|
||||
"analytics.project-status.unlisted": {
|
||||
"message": "Verborgen"
|
||||
},
|
||||
"analytics.project-status.withheld": {
|
||||
"message": "Niet weergegeven"
|
||||
},
|
||||
"analytics.project.all": {
|
||||
"message": "Alle projecten"
|
||||
},
|
||||
@@ -308,6 +383,9 @@
|
||||
"analytics.query.filter.add": {
|
||||
"message": "Filter toevoegen"
|
||||
},
|
||||
"analytics.query.label.breakdown": {
|
||||
"message": "Uitsplitsing:"
|
||||
},
|
||||
"analytics.query.label.grouped-by": {
|
||||
"message": "Groepeer per"
|
||||
},
|
||||
@@ -320,6 +398,9 @@
|
||||
"analytics.stat.downloads": {
|
||||
"message": "Installaties"
|
||||
},
|
||||
"analytics.stat.monetization-banner.body": {
|
||||
"message": "Alleen weergaven en downloads die via Modrinth plaatsvinden, komen in aanmerking voor inkomsten en moeten de fraudepreventiefiltering doorstaan. Voor downloads van de Modrinth-app moet de gebruiker bovendien ingelogd zijn. Aangezien alle projecten een vergelijkbaar percentage aan downloads met inkomsten hebben, zouden je inkomsten niet wezenlijk veranderen als alle downloads zouden worden meegeteld."
|
||||
},
|
||||
"analytics.stat.monetization-banner.learn-more": {
|
||||
"message": "Meer informatie"
|
||||
},
|
||||
@@ -332,6 +413,12 @@
|
||||
"analytics.stat.playtime-hours": {
|
||||
"message": "{hours} uur"
|
||||
},
|
||||
"analytics.stat.previous-period-comparison": {
|
||||
"message": "ten opzichte van vorige periode"
|
||||
},
|
||||
"analytics.stat.previous-period-comparison-short": {
|
||||
"message": "ten opzichte van vorige"
|
||||
},
|
||||
"analytics.stat.revenue": {
|
||||
"message": "Inkomsten"
|
||||
},
|
||||
@@ -341,12 +428,30 @@
|
||||
"analytics.stat.unavailable": {
|
||||
"message": "n.v.t."
|
||||
},
|
||||
"analytics.stat.unavailable-tooltip": {
|
||||
"message": "Geen gegevens voor huidige zoekopdracht beschikbaar"
|
||||
},
|
||||
"analytics.stat.views": {
|
||||
"message": "Weergaven"
|
||||
},
|
||||
"analytics.table.csv.date-range": {
|
||||
"message": "{start} tot {end}"
|
||||
},
|
||||
"analytics.table.csv.filename": {
|
||||
"message": "Modrinth Analytics {breakdown} Uitsplitsing - {dateRange}"
|
||||
},
|
||||
"analytics.table.csv.header.playtime-seconds": {
|
||||
"message": "Speeltijd (seconden)"
|
||||
},
|
||||
"analytics.table.duration.days": {
|
||||
"message": "{count, plural, one {# dag} other {# dagen}}"
|
||||
},
|
||||
"analytics.table.duration.hours": {
|
||||
"message": "{count, plural, one {# uur} other {# uren}}"
|
||||
},
|
||||
"analytics.table.duration.minutes": {
|
||||
"message": "{count, plural, one {# minuut} other {# minuten}}"
|
||||
},
|
||||
"analytics.table.empty.no-matching-rows": {
|
||||
"message": "Geen passende gegevens"
|
||||
},
|
||||
@@ -371,12 +476,42 @@
|
||||
"analytics.threshold.country-downloads-aria": {
|
||||
"message": "Land download limiet"
|
||||
},
|
||||
"analytics.threshold.game-version-downloads-aria": {
|
||||
"message": "Drempelwaarde voor aantal downloads van spelversies"
|
||||
},
|
||||
"analytics.threshold.game-versions-above": {
|
||||
"message": "Spelversies met meer dan"
|
||||
},
|
||||
"analytics.threshold.project-downloads-aria": {
|
||||
"message": "Drempelwaarde voor projectdownloads"
|
||||
},
|
||||
"analytics.threshold.project-version-downloads-aria": {
|
||||
"message": "Drempelwaarde voor aantal downloads van projectversies"
|
||||
},
|
||||
"analytics.threshold.project-versions-above": {
|
||||
"message": "Projectversies met meer dan"
|
||||
},
|
||||
"analytics.threshold.projects-above": {
|
||||
"message": "Projecten boven"
|
||||
},
|
||||
"analytics.title": {
|
||||
"message": "Analytica"
|
||||
},
|
||||
"analytics.value.monetized": {
|
||||
"message": "Gemonetiseerd"
|
||||
},
|
||||
"analytics.value.no-dependent": {
|
||||
"message": "Geen"
|
||||
},
|
||||
"analytics.value.no-dependent-tooltip": {
|
||||
"message": "Gedownload om andere redenen dan dat het een afhankelijkheid is"
|
||||
},
|
||||
"analytics.value.none": {
|
||||
"message": "Geen"
|
||||
},
|
||||
"analytics.value.other": {
|
||||
"message": "Anders"
|
||||
},
|
||||
"analytics.value.unknown": {
|
||||
"message": "Onbekend"
|
||||
},
|
||||
@@ -1574,12 +1709,24 @@
|
||||
"dashboard.projects.selection-bar.aria-label": {
|
||||
"message": ""
|
||||
},
|
||||
"dashboard.projects.selection-bar.selected-count": {
|
||||
"message": "{count, plural, one {# geselecteerd project} other {# geselecteerde projecten}}"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
"dashboard.projects.table.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
"dashboard.projects.table.select-all-bulk-editable": {
|
||||
"message": "Selecteer alle projecten die bulkbewerking ondersteunen"
|
||||
},
|
||||
"dashboard.projects.table.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.table.type": {
|
||||
"message": "Type"
|
||||
},
|
||||
"dashboard.report.title": {
|
||||
"message": "{id} rapporteren"
|
||||
},
|
||||
@@ -2519,6 +2666,12 @@
|
||||
"layout.nav.upgrade-to-modrinth-plus": {
|
||||
"message": "Upgrade naar Modrinth+"
|
||||
},
|
||||
"layout.publish.email-verification-required.description": {
|
||||
"message": "Je moet je e-mailadres verifiëren voordat je iets op Modrinth kunt publiceren."
|
||||
},
|
||||
"layout.publish.email-verification-required.title": {
|
||||
"message": "E-mailverificatie vereist"
|
||||
},
|
||||
"moderation.exclude-technical-review": {
|
||||
"message": "TR uitsluiten"
|
||||
},
|
||||
@@ -2843,15 +2996,15 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Controleer uw portemonnee adres nogmaals. Geld dat naar een onjuist adres is overgemaakt, kan niet worden teruggevonden."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "De wijzigingen worden toegepast op {count, plural, one {# project} other {# projects}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Een Modrinth creator."
|
||||
},
|
||||
"profile.bio.fallback.user": {
|
||||
"message": "Een Modrinth gebruiker."
|
||||
},
|
||||
"profile.button.analytics": {
|
||||
"message": "Gebruikersstatistieken bekijken"
|
||||
},
|
||||
"profile.button.billing": {
|
||||
"message": "Gebruikers facturering beheren"
|
||||
},
|
||||
@@ -2870,6 +3023,9 @@
|
||||
"profile.button.set-affiliate": {
|
||||
"message": "Zet als partner"
|
||||
},
|
||||
"profile.collection.projects-count": {
|
||||
"message": "{count, plural, one {# project} other {# projecten}}"
|
||||
},
|
||||
"profile.details.label.auth-providers": {
|
||||
"message": "Beveiliging providers"
|
||||
},
|
||||
@@ -2906,9 +3062,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Collectie"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {download} other {downloads}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Lid geworden"
|
||||
},
|
||||
@@ -2927,9 +3080,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organisaties"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {project} other {projecten}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Opslaan..."
|
||||
},
|
||||
@@ -2939,6 +3089,12 @@
|
||||
"profile.meta.description-with-bio": {
|
||||
"message": "{bio} - Download {username}'s projecten op Modrinth"
|
||||
},
|
||||
"profile.official-account": {
|
||||
"message": "Officieel Modrinth-account"
|
||||
},
|
||||
"profile.official-account.bio": {
|
||||
"message": "Het officiële gebruikersaccount van Modrinth. Neem contact op met het Helpcentrum via <support-link></support-link> of per e-mail via <email></email>"
|
||||
},
|
||||
"profile.stats.projects-followers": {
|
||||
"message": "{count, plural, one {<stat>{count}</stat> project volger}other {<stat>{count}</stat> project volgers}}"
|
||||
},
|
||||
@@ -3110,15 +3266,108 @@
|
||||
"project.details.licensed": {
|
||||
"message": "Gelicentieerd"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Deze spelversie is niet compatibel met het basisproject."
|
||||
},
|
||||
"project.download.base-loader-incompatible-tooltip": {
|
||||
"message": "Deze loader is niet compatibel met het basisproject."
|
||||
},
|
||||
"project.download.compatible-version-title": {
|
||||
"message": "Compatibele versies"
|
||||
},
|
||||
"project.download.dependencies-title": {
|
||||
"message": "Afhankelijkheden"
|
||||
},
|
||||
"project.download.dependency-already-installed": {
|
||||
"message": "Deze afhankelijkheid is al geïnstalleerd"
|
||||
},
|
||||
"project.download.dependency-any-compatible": {
|
||||
"message": "Elke compatibele"
|
||||
},
|
||||
"project.download.dependency-conflicting": {
|
||||
"message": "Deze afhankelijkheid is in conflict met een andere afhankelijkheid"
|
||||
},
|
||||
"project.download.dependency-download-file": {
|
||||
"message": "{filename} downloaden"
|
||||
},
|
||||
"project.download.dependency-download-file-with-size": {
|
||||
"message": "{filename} downloaden ({size})"
|
||||
},
|
||||
"project.download.dependency-duplicate": {
|
||||
"message": "Deze afhankelijkheid is al opgenomen"
|
||||
},
|
||||
"project.download.dependency-missing-version": {
|
||||
"message": "Deze versie van de afhankelijkheid is niet beschikbaar"
|
||||
},
|
||||
"project.download.dependency-no-compatible-version": {
|
||||
"message": "Er is geen compatibele versie beschikbaar voor deze afhankelijkheid"
|
||||
},
|
||||
"project.download.dependency-quilt-fabric-api": {
|
||||
"message": "Fabric API wordt voor Quilt overgeslagen"
|
||||
},
|
||||
"project.download.dependency-resource-pack-admonition": {
|
||||
"message": "Dit project is afhankelijk van een vereist bronpakket. Download het en plaats het in je {folder} map."
|
||||
},
|
||||
"project.download.dependency-unavailable": {
|
||||
"message": "Deze afhankelijkheid kan niet worden gedownload"
|
||||
},
|
||||
"project.download.download": {
|
||||
"message": "Downloaden"
|
||||
},
|
||||
"project.download.download-as-zip": {
|
||||
"message": "Als .zip downloaden"
|
||||
},
|
||||
"project.download.download-version": {
|
||||
"message": "{version} downloaden"
|
||||
},
|
||||
"project.download.download-with-dependencies": {
|
||||
"message": "Downloaden met afhankelijkheden"
|
||||
},
|
||||
"project.download.download-with-recommended": {
|
||||
"message": "Met aanbevolen instellingen downloaden"
|
||||
},
|
||||
"project.download.download-with-recommended-as-zip": {
|
||||
"message": "Met aanbevolen instellingen als .zip downloaden"
|
||||
},
|
||||
"project.download.duplicate-dependencies-hidden": {
|
||||
"message": "Dubbele afhankelijkheden worden verborgen"
|
||||
},
|
||||
"project.download.file-unavailable": {
|
||||
"message": "Dit bestand kan niet worden gedownload"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} steunt niet {gameVersion} voor {platform}"
|
||||
},
|
||||
"project.download.install-with-app": {
|
||||
"message": "Via de Modrinth-app installeren"
|
||||
},
|
||||
"project.download.manually": {
|
||||
"message": "Handmatig downloaden"
|
||||
},
|
||||
"project.download.no-app": {
|
||||
"message": "Heb je de Modrinth App niet?"
|
||||
},
|
||||
"project.download.no-game-versions-found": {
|
||||
"message": "Geen spelversies gevonden"
|
||||
},
|
||||
"project.download.no-versions-available": {
|
||||
"message": "Geen versies beschikbaar voor {gameVersion} en {platform}."
|
||||
},
|
||||
"project.download.platform-unsupported-tooltip": {
|
||||
"message": "{title} steunt niet {platform} voor {gameVersion}"
|
||||
},
|
||||
"project.download.recommended-title": {
|
||||
"message": "Aanbevolen"
|
||||
},
|
||||
"project.download.required-resource-pack-admonition": {
|
||||
"message": "Voor dit gegevenspakket is ook een bronpakket nodig. Download het en plaats het in je {folder} map."
|
||||
},
|
||||
"project.download.required-resource-pack-short": {
|
||||
"message": "Bronpakket"
|
||||
},
|
||||
"project.download.search-game-versions": {
|
||||
"message": "Spelversie kiezen"
|
||||
},
|
||||
"project.download.select-game-version": {
|
||||
"message": "Selecteer game-versie"
|
||||
},
|
||||
@@ -3257,6 +3506,15 @@
|
||||
"project.settings.permissions.attention-needed.title": {
|
||||
"message": "Onbekende externe inhoud"
|
||||
},
|
||||
"project.settings.permissions.sort.most-files": {
|
||||
"message": "Meeste bestanden"
|
||||
},
|
||||
"project.settings.permissions.sort.recently-edited": {
|
||||
"message": "Onlangs bewerkt"
|
||||
},
|
||||
"project.settings.permissions.sort.rejected": {
|
||||
"message": "Afgewezen"
|
||||
},
|
||||
"project.settings.title": {
|
||||
"message": "Instellingen"
|
||||
},
|
||||
@@ -3330,10 +3588,10 @@
|
||||
"message": "Melding indienen"
|
||||
},
|
||||
"scopes.analytics.description": {
|
||||
"message": "Krijg toegang tot jouw analysegegevens"
|
||||
"message": "Bekijk je analysegegevens"
|
||||
},
|
||||
"scopes.analytics.label": {
|
||||
"message": "Lees analyses"
|
||||
"message": "Analyses bekijken"
|
||||
},
|
||||
"scopes.category.analytics": {
|
||||
"message": "Statistieken"
|
||||
@@ -3456,10 +3714,10 @@
|
||||
"message": "Schrijf betalingen"
|
||||
},
|
||||
"scopes.performAnalytics.description": {
|
||||
"message": "Voer analyserende handelingen uit"
|
||||
"message": "Analytische acties uitvoeren"
|
||||
},
|
||||
"scopes.performAnalytics.label": {
|
||||
"message": "Voer analyses uit"
|
||||
"message": "Analyses uitvoeren"
|
||||
},
|
||||
"scopes.projectCreate.description": {
|
||||
"message": "Nieuwe projecten aanmaken"
|
||||
@@ -4357,5 +4615,8 @@
|
||||
},
|
||||
"version.environment.unknown.title": {
|
||||
"message": "Onbekende omgeving"
|
||||
},
|
||||
"version.section.content.dev-info.maven-note": {
|
||||
"message": "Opmerking: Indien beschikbaar, raden we je aan om de Maven-repository van de maker te gebruiken, aangezien deze informatie over transitieve afhankelijkheden bevat die de Modrinth Maven API niet biedt. Bovendien kun je te maken krijgen met dubbele afhankelijkheden als je voor je afhankelijkheden zowel Modrinth- als niet-Modrinth-Maven-repositories gebruikt, omdat de groeps-ID anders zal zijn wanneer deze via de Modrinth Maven API wordt aangeboden."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2609,9 +2609,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Dobbeltsjekk lommeboksadressa di. Penger sendt til feil adresse kan ikke bli gjenoppretta."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Endringer blir utført på {count, plural, one {prosjekt} other {prosjekter}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "En Modrinth-skaper."
|
||||
},
|
||||
@@ -2678,9 +2675,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Samling"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {nedlasting} other {nedlastinger}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Blei med"
|
||||
},
|
||||
@@ -2699,9 +2693,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organisasjoner"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {projekt} other {projekt}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Lagrer..."
|
||||
},
|
||||
@@ -3038,12 +3029,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Besøk prosjekt-dashbordet"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {nedlasting} other {nedlastinger}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {følger} other {følgere}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} har blitt arkivert. {title} kommer ikke til å få noen nye oppdateringer, hvis ikke forfatteren bestemmer seg for å dearkivere prosjektet."
|
||||
},
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
"message": "Pokaż szczegóły analityki monetyzacji"
|
||||
},
|
||||
"analytics.chart.legend.monetization-details.description": {
|
||||
"message": "Tylko wyświetlenia i pobrania z Modrinth liczą się do monetyzacji, gdzie pobrania wymagają, że użytkownik jest zalogowany."
|
||||
"message": "Tylko wyświetlenia i pobrania z Modrinth liczą się do monetyzacji, gdzie pobrania wymagają bycia zalogowanym."
|
||||
},
|
||||
"analytics.chart.legend.monetization-details.title": {
|
||||
"message": "Szczegóły analityki monetyzacji"
|
||||
@@ -1029,7 +1029,7 @@
|
||||
"message": "Wyślij "
|
||||
},
|
||||
"conversation-thread.action.send-to-review": {
|
||||
"message": "Wyślij do sprawdzenia"
|
||||
"message": "Wyślij do przeglądu"
|
||||
},
|
||||
"conversation-thread.action.send-to-review-with-reply": {
|
||||
"message": "Wyślij do przeglądu z odpowiedzią"
|
||||
@@ -2993,9 +2993,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Sprawdź podwójnie adres portfela. Fundusze przesłane na nieprawidłowy portfel nie mogą być odzyskane."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Zmiany zostaną zastosowane dla {count, plural, one {# projektu} other {# projektów}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Twórca na Modrinth."
|
||||
},
|
||||
@@ -3062,9 +3059,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Kolekcja"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {pobranie} few {pobrania} other {pobrań}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Dołączył(-a)"
|
||||
},
|
||||
@@ -3083,9 +3077,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizacje"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {projekt} few {projekty} other {projektów}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Zapisywanie..."
|
||||
},
|
||||
@@ -3647,12 +3638,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Odwiedź pulpit projektów"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {pobranie} few {pobrania} other {pobrań}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {obserwujący} other {obserwujących}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} zostało zarchiwizowane. {title} nie będzie otrzymywać żadnych dalszych aktualizacji, chyba że autor zdecyduje się przywrócić projekt."
|
||||
},
|
||||
|
||||
@@ -2672,6 +2672,12 @@
|
||||
"layout.nav.upgrade-to-modrinth-plus": {
|
||||
"message": "Assinar o Modrinth+"
|
||||
},
|
||||
"layout.publish.email-verification-required.description": {
|
||||
"message": "Você precisa verificar seu e-mail antes de publicar no Modrinth."
|
||||
},
|
||||
"layout.publish.email-verification-required.title": {
|
||||
"message": "Verificação de e-mail necessária"
|
||||
},
|
||||
"moderation.exclude-technical-review": {
|
||||
"message": "Excluir RT"
|
||||
},
|
||||
@@ -2999,9 +3005,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Verifique novamente seu endereço da carteira. Fundos enviados para um endereço incorreto não podem ser recuperados."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "As alterações serão aplicadas a {count, plural, one {# projeto} other {# projetos}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Um criador Modrinth."
|
||||
},
|
||||
@@ -3650,12 +3653,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Visitar painel de projetos"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, =0 {Nenhum download} one {# download} other {# downloads}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, =0 {Nenhum seguidor} one {# seguidor} other {# seguidores}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} foi arquivado. {title} não receberá atualizações a menos que o autor decida desarquivar o projeto."
|
||||
},
|
||||
|
||||
@@ -2561,9 +2561,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Coleção"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {transferência} other {transferências}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Entrou"
|
||||
},
|
||||
@@ -2582,9 +2579,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizações"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural,one {projeto} other {projetos}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "A guardar..."
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"message": "Разбивка: {breakdown}"
|
||||
},
|
||||
"analytics.chart.action.show-all": {
|
||||
"message": "Показать все"
|
||||
"message": "Показать всё"
|
||||
},
|
||||
"analytics.chart.action.show-limited": {
|
||||
"message": "Показать меньше"
|
||||
@@ -114,7 +114,7 @@
|
||||
"message": "Доли"
|
||||
},
|
||||
"analytics.chart.empty.select-table-items": {
|
||||
"message": "Выберите элементы из таблицы ниже, чтобы отобразить данные."
|
||||
"message": "Выберите элементы из таблицы ниже, чтобы отобразить данные."
|
||||
},
|
||||
"analytics.chart.events.count-aria": {
|
||||
"message": "{count, plural, one {# событие аналитики} few {# события аналитики} other {# событий аналитики}}"
|
||||
@@ -123,7 +123,7 @@
|
||||
"message": "<project>{projectName}</project>: {title}"
|
||||
},
|
||||
"analytics.chart.events.see-announcement": {
|
||||
"message": "Перейти к объявлению"
|
||||
"message": "Перейти к объявлению"
|
||||
},
|
||||
"analytics.chart.legend.monetization-details.aria": {
|
||||
"message": "Посмотреть сведения о монетизации"
|
||||
@@ -138,7 +138,7 @@
|
||||
"message": "{name} (пред.)"
|
||||
},
|
||||
"analytics.chart.render-limit.description": {
|
||||
"message": "Если показать все выбранные строки, страница может работать медленнее."
|
||||
"message": "Отображение всех выбранных строк из таблицы может замедлить страницу."
|
||||
},
|
||||
"analytics.chart.table-selection.all": {
|
||||
"message": "Показаны все {itemType, select,\n project {проекты}\n country {страны}\n monetization {типы монетизации}\n downloadSource {источники скачивания}\n downloadReason {причины скачивания}\n member {участники}\n projectVersion {версии проекта}\n loader {загрузчики}\n gameVersion {версии игры}\n other {элементы}\n} из таблицы"
|
||||
@@ -171,7 +171,7 @@
|
||||
"message": "Скрыть {name} на диаграмме"
|
||||
},
|
||||
"analytics.chart.tooltip.pinned": {
|
||||
"message": "Подсказка диаграммы закреплена"
|
||||
"message": "Подсказка графика закреплена"
|
||||
},
|
||||
"analytics.chart.tooltip.pinned-aria": {
|
||||
"message": "Закреплено"
|
||||
@@ -210,7 +210,7 @@
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"analytics.download-source.website": {
|
||||
"message": "Сайт Modrinth"
|
||||
"message": "Веб-сайт Modrinth"
|
||||
},
|
||||
"analytics.downloads.suffix": {
|
||||
"message": "скачиваний"
|
||||
@@ -798,7 +798,7 @@
|
||||
"message": "Отправить письмо"
|
||||
},
|
||||
"auth.reset-password.method-choice.description": {
|
||||
"message": "Введите свою почту, чтобы получить ссылку для восстановления доступа к аккаунту."
|
||||
"message": "Введите почту, чтобы получить ссылку для восстановления доступа к аккаунту."
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.text": {
|
||||
"message": "Если почта привязана к аккаунту, на неё придёт письмо с инструкциями."
|
||||
@@ -2666,6 +2666,12 @@
|
||||
"layout.nav.upgrade-to-modrinth-plus": {
|
||||
"message": "Перейти на Modrinth+"
|
||||
},
|
||||
"layout.publish.email-verification-required.description": {
|
||||
"message": "Перед публикацией на Modrinth необходимо подтвердить адрес электронной почты."
|
||||
},
|
||||
"layout.publish.email-verification-required.title": {
|
||||
"message": "Необходимо подтвердить почту"
|
||||
},
|
||||
"moderation.exclude-technical-review": {
|
||||
"message": "Исключать ТП"
|
||||
},
|
||||
@@ -2990,9 +2996,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Перепроверьте адрес кошелька. Отправку средств на неверный адрес невозможно отменить."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Изменения будут применены к {count, plural, one {# проекту} other {# проектам}}."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Автор на Modrinth."
|
||||
},
|
||||
@@ -3059,9 +3062,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Коллекция"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {скачивание} few {скачивания} other {скачиваний}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Регистрация:"
|
||||
},
|
||||
@@ -3080,9 +3080,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Организации"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {проект} few {проекта} other {проектов}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Сохранение..."
|
||||
},
|
||||
@@ -3647,12 +3644,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Управление проектами"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {загрузка} few {загрузки} other {загрузок}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {подписчик} few {подписчика} other {подписчиков}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} помещён в архив. {title} больше не будет получать обновления, если только автор не решит разархивировать проект."
|
||||
},
|
||||
|
||||
@@ -101,6 +101,12 @@
|
||||
"analytics.chart.controls.modrinth-events": {
|
||||
"message": "Modrinth događaji"
|
||||
},
|
||||
"analytics.chart.controls.no-modrinth-events": {
|
||||
"message": "Nema Modrinth događaja na grafu."
|
||||
},
|
||||
"analytics.chart.controls.no-project-events": {
|
||||
"message": "Nema događaja projekta u grafu."
|
||||
},
|
||||
"analytics.chart.controls.previous-period": {
|
||||
"message": "Prethodni period"
|
||||
},
|
||||
@@ -110,6 +116,15 @@
|
||||
"analytics.chart.controls.ratio": {
|
||||
"message": "Odnos"
|
||||
},
|
||||
"analytics.chart.empty.select-table-items": {
|
||||
"message": "Izaberi stavke iz tabele ispod da bi prikazao svoje podatke."
|
||||
},
|
||||
"analytics.chart.events.count-aria": {
|
||||
"message": "{count, plural, one {# događaj analitike} other{# događaja analitike}}"
|
||||
},
|
||||
"analytics.chart.events.project-title": {
|
||||
"message": "<project>{projectName}</project>: {title}"
|
||||
},
|
||||
"analytics.chart.events.see-announcement": {
|
||||
"message": "Pogledaj obaveštenje"
|
||||
},
|
||||
@@ -167,15 +182,48 @@
|
||||
"analytics.download-source.app": {
|
||||
"message": "Modrinth aplikacija"
|
||||
},
|
||||
"analytics.download-source.website": {
|
||||
"message": "Modrinth veb-stranica"
|
||||
},
|
||||
"analytics.downloads.suffix": {
|
||||
"message": "preuzimanja"
|
||||
},
|
||||
"analytics.filter.game-version-type": {
|
||||
"message": "Tip verzije igre"
|
||||
},
|
||||
"analytics.filter.game-version-type.all": {
|
||||
"message": "Sve"
|
||||
},
|
||||
"analytics.filter.game-version-type.release": {
|
||||
"message": "Izdanja"
|
||||
},
|
||||
"analytics.filter.search.countries": {
|
||||
"message": "Traži države..."
|
||||
},
|
||||
"analytics.filter.search.dependent-projects": {
|
||||
"message": "Traži projekte..."
|
||||
},
|
||||
"analytics.filter.search.download-sources": {
|
||||
"message": "Traži izvore preuzimanja..."
|
||||
},
|
||||
"analytics.filter.search.members": {
|
||||
"message": "Traži članove..."
|
||||
},
|
||||
"analytics.filter.search.project-versions": {
|
||||
"message": "Traži verzije projekta..."
|
||||
},
|
||||
"analytics.filter.search.versions": {
|
||||
"message": "Traži verzije..."
|
||||
},
|
||||
"analytics.graph.title.downloads": {
|
||||
"message": "Preuzimanja preko vremena"
|
||||
},
|
||||
"analytics.graph.title.playtime": {
|
||||
"message": "Vreme igranja preko vremena"
|
||||
},
|
||||
"analytics.graph.title.views": {
|
||||
"message": "Pregledi preko vremena"
|
||||
},
|
||||
"analytics.group-by.1h": {
|
||||
"message": "1 sat"
|
||||
},
|
||||
@@ -215,6 +263,51 @@
|
||||
"analytics.group-by.year": {
|
||||
"message": "Godina"
|
||||
},
|
||||
"analytics.options.loading": {
|
||||
"message": "Učitavanje..."
|
||||
},
|
||||
"analytics.project-event.project-approved": {
|
||||
"message": "Projekat odobren"
|
||||
},
|
||||
"analytics.project-event.project-unlisted": {
|
||||
"message": "Projekat nenaveden"
|
||||
},
|
||||
"analytics.project-event.version-released": {
|
||||
"message": "{version} izdana"
|
||||
},
|
||||
"analytics.project-event.version-uploaded": {
|
||||
"message": "Verzija postavljena"
|
||||
},
|
||||
"analytics.project-status.approved": {
|
||||
"message": "Odobreno"
|
||||
},
|
||||
"analytics.project-status.archived": {
|
||||
"message": "Arhivirano"
|
||||
},
|
||||
"analytics.project-status.draft": {
|
||||
"message": "Nacrt"
|
||||
},
|
||||
"analytics.project-status.other": {
|
||||
"message": "Drugo"
|
||||
},
|
||||
"analytics.project-status.private": {
|
||||
"message": "Privatno"
|
||||
},
|
||||
"analytics.project-status.rejected": {
|
||||
"message": "Odbijeno"
|
||||
},
|
||||
"analytics.project-status.unlisted": {
|
||||
"message": "Nenavedeno"
|
||||
},
|
||||
"analytics.project-status.withheld": {
|
||||
"message": "Zadržano"
|
||||
},
|
||||
"analytics.project.all": {
|
||||
"message": "Svi projekati"
|
||||
},
|
||||
"analytics.project.count": {
|
||||
"message": "{count, plural, one {# projekat} few {# projekta} other {# projekata}}"
|
||||
},
|
||||
"analytics.project.icon-alt": {
|
||||
"message": "Ikona {name}"
|
||||
},
|
||||
@@ -230,12 +323,21 @@
|
||||
"analytics.query.filter.add": {
|
||||
"message": "Dodaj filter"
|
||||
},
|
||||
"analytics.query.label.breakdown": {
|
||||
"message": "Raščlanjivanje:"
|
||||
},
|
||||
"analytics.query.label.grouped-by": {
|
||||
"message": "Grupirano po"
|
||||
},
|
||||
"analytics.query.label.project": {
|
||||
"message": "Projekat:"
|
||||
},
|
||||
"analytics.query.label.timeframe": {
|
||||
"message": "Okvir vremena:"
|
||||
},
|
||||
"analytics.stat.downloads": {
|
||||
"message": "Preuzimanja"
|
||||
},
|
||||
"analytics.stat.monetization-banner.learn-more": {
|
||||
"message": "Saznaj više"
|
||||
},
|
||||
@@ -260,24 +362,48 @@
|
||||
"analytics.stat.unavailable": {
|
||||
"message": "N/A"
|
||||
},
|
||||
"analytics.stat.views": {
|
||||
"message": "Pogledi"
|
||||
},
|
||||
"analytics.table.csv.date-range": {
|
||||
"message": "Od {start} do {end}"
|
||||
},
|
||||
"analytics.table.csv.header.playtime-seconds": {
|
||||
"message": "Vreme igranja (sekunde)"
|
||||
},
|
||||
"analytics.table.csv.selected-range": {
|
||||
"message": "Izabran okvir"
|
||||
},
|
||||
"analytics.table.duration.days": {
|
||||
"message": "{count, plural, one {# dan} other {# dana}}"
|
||||
},
|
||||
"analytics.table.duration.hours": {
|
||||
"message": "{count, plural, one {# sat} few {# sata} other {# sati}}"
|
||||
},
|
||||
"analytics.table.empty.no-matching-rows": {
|
||||
"message": "Nema odgovarajućih redova za analitiku"
|
||||
},
|
||||
"analytics.table.export.cumulative": {
|
||||
"message": "Komulativno"
|
||||
},
|
||||
"analytics.table.export.grouped": {
|
||||
"message": "Grupirano po {groupBy}"
|
||||
},
|
||||
"analytics.table.pagination.summary": {
|
||||
"message": "Pokazivanje ukupno od {start} do {end} od {total}"
|
||||
},
|
||||
"analytics.table.search.placeholder": {
|
||||
"message": "Pretraži..."
|
||||
},
|
||||
"analytics.threshold.country-downloads-aria": {
|
||||
"message": "Prag preuzimanja po državi"
|
||||
},
|
||||
"analytics.title": {
|
||||
"message": "Analitike"
|
||||
},
|
||||
"analytics.value.monetized": {
|
||||
"message": "Monetizovano"
|
||||
},
|
||||
"analytics.value.no-dependent": {
|
||||
"message": "Bez"
|
||||
},
|
||||
@@ -290,6 +416,9 @@
|
||||
"analytics.value.unknown": {
|
||||
"message": "Nepoznato"
|
||||
},
|
||||
"analytics.value.unmonetized": {
|
||||
"message": "Bez monetizacije"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "Naša desktop aplikacija je dostupna na svim platformama, izaberi željenu verziju."
|
||||
},
|
||||
@@ -329,6 +458,15 @@
|
||||
"app-marketing.features.follow.title": {
|
||||
"message": "Prati projekte"
|
||||
},
|
||||
"app-marketing.features.importing.gdlauncher-alt": {
|
||||
"message": "GDLauncher"
|
||||
},
|
||||
"app-marketing.features.importing.multimc-alt": {
|
||||
"message": "MultiMC"
|
||||
},
|
||||
"app-marketing.features.mod-management.description": {
|
||||
"message": "Modrinth olakšava upravljanje svim tvojim modovima na jednom mestu. Modove možeš da instaliraš, uklanjaš i ažuriraš samo jednim klikom."
|
||||
},
|
||||
"app-marketing.features.mod-management.installed-mods": {
|
||||
"message": "Instalirani modovi"
|
||||
},
|
||||
@@ -347,24 +485,84 @@
|
||||
"app-marketing.features.offline.description": {
|
||||
"message": "Igraj svoje modove, bez obzira da li si povezani na internet ili ne."
|
||||
},
|
||||
"app-marketing.features.offline.title": {
|
||||
"message": "Oflajn režim"
|
||||
},
|
||||
"app-marketing.features.open-source.title": {
|
||||
"message": "Otvoreni kod"
|
||||
},
|
||||
"app-marketing.features.performance.activity-monitor": {
|
||||
"message": "Praćenje aktivnosti"
|
||||
},
|
||||
"app-marketing.features.performance.cpu-percent": {
|
||||
"message": "% procesora"
|
||||
},
|
||||
"app-marketing.features.performance.discord": {
|
||||
"message": "Discord"
|
||||
},
|
||||
"app-marketing.features.performance.good-performance": {
|
||||
"message": "Dobar performanse"
|
||||
},
|
||||
"app-marketing.features.performance.google-chrome": {
|
||||
"message": "Google Chrome"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-mb": {
|
||||
"message": "∞ MB"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-times-infinite-mb": {
|
||||
"message": "∞ × ∞ MB"
|
||||
},
|
||||
"app-marketing.features.performance.less-than-150mb": {
|
||||
"message": "< 150 MB"
|
||||
},
|
||||
"app-marketing.features.performance.modrinth-app": {
|
||||
"message": "Modrinth aplikacija"
|
||||
},
|
||||
"app-marketing.features.performance.one-billion-percent": {
|
||||
"message": "1 milijarda %"
|
||||
},
|
||||
"app-marketing.features.performance.process-name": {
|
||||
"message": "Ime procesa"
|
||||
},
|
||||
"app-marketing.features.performance.ram": {
|
||||
"message": "RAM / radna memorija"
|
||||
},
|
||||
"app-marketing.features.performance.small": {
|
||||
"message": "Malo"
|
||||
},
|
||||
"app-marketing.features.sharing.modpack": {
|
||||
"message": "Modpack"
|
||||
},
|
||||
"app-marketing.features.sharing.share-button": {
|
||||
"message": "Deli"
|
||||
},
|
||||
"app-marketing.features.sharing.title": {
|
||||
"message": "Deli modpackove"
|
||||
},
|
||||
"app-marketing.hero.app-screenshot-alt": {
|
||||
"message": "Snimak ekrana Modrinth aplikacije sa otvorenom instancom Cobblemona na stranici Sadržaja."
|
||||
},
|
||||
"app-marketing.hero.download-button": {
|
||||
"message": "Preuzmi Modrinth aplikaciju"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app": {
|
||||
"message": "Preuzmi Modrinth aplikaciju"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app-for-os": {
|
||||
"message": "Preuzmi Modrinth aplikaciju za {os}"
|
||||
},
|
||||
"app-marketing.hero.minecraft-screenshot-alt": {
|
||||
"message": "Snimak ekrana glavnog menija instance Cobblemona."
|
||||
},
|
||||
"app-marketing.hero.more-download-options": {
|
||||
"message": "Više opcije preuzimanja"
|
||||
},
|
||||
"app-marketing.hide-other-packages": {
|
||||
"message": "Krij druge pakete"
|
||||
},
|
||||
"app-marketing.show-other-packages": {
|
||||
"message": "Pokazuj druge pakete"
|
||||
},
|
||||
"auth.authorize.action.authorize": {
|
||||
"message": "Odobri"
|
||||
},
|
||||
@@ -380,9 +578,36 @@
|
||||
"auth.authorize.error.no-redirect-url": {
|
||||
"message": "Nije pronađena lokacija za preusmeravanje u odgovoru"
|
||||
},
|
||||
"auth.authorize.errro-title": {
|
||||
"message": "Došlo je do greške"
|
||||
},
|
||||
"auth.authorize.redirect-url": {
|
||||
"message": "Bićeš preusmeren na <redirect-url>{url}</redirect-url>"
|
||||
},
|
||||
"auth.continue-with-provider": {
|
||||
"message": "Nastavi sa {provider}"
|
||||
},
|
||||
"auth.create-account.complete-sign-up": {
|
||||
"message": "Završi prijavu"
|
||||
},
|
||||
"auth.create-account.date-of-birth.invalid.text": {
|
||||
"message": "Molimo upiši važeći datum rođenja. Godina nemože biti 0000."
|
||||
},
|
||||
"auth.create-account.date-of-birth.invalid.title": {
|
||||
"message": "Nevažeći datum rođenja"
|
||||
},
|
||||
"auth.create-account.date-of-birth.label": {
|
||||
"message": "Datum rođenja"
|
||||
},
|
||||
"auth.create-account.date-of-birth.placeholder": {
|
||||
"message": "Ukucaj svoj datum rođenja"
|
||||
},
|
||||
"auth.create-account.date-of-birth.required.title": {
|
||||
"message": "Potreban je datum rođenja"
|
||||
},
|
||||
"auth.create-account.page-title": {
|
||||
"message": "Kreiraj račun"
|
||||
},
|
||||
"auth.reset-password.method-choice.action": {
|
||||
"message": "Pošalji email za oporavak"
|
||||
},
|
||||
@@ -503,6 +728,12 @@
|
||||
"create.project.type-server": {
|
||||
"message": "Server"
|
||||
},
|
||||
"dashboard.analytics.total-downloads": {
|
||||
"message": "Ukupan broj preuzimanja"
|
||||
},
|
||||
"dashboard.analytics.total-followers": {
|
||||
"message": "Ukupan broj pratilaca"
|
||||
},
|
||||
"dashboard.collections.button.create-new": {
|
||||
"message": "Kreiraj novo"
|
||||
},
|
||||
@@ -512,6 +743,12 @@
|
||||
"dashboard.collections.long-title": {
|
||||
"message": "Tvoje kolekcije"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.method-selection.country-placeholder": {
|
||||
"message": "Izaberi svoju državu"
|
||||
},
|
||||
"dashboard.creator-withdraw-modal.method-selection.country-search-placeholder": {
|
||||
"message": "Traži države..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverski projekti ne podržavaju grupno uređivanje"
|
||||
},
|
||||
@@ -527,6 +764,15 @@
|
||||
"dashboard.projects.links.placeholder.cleared": {
|
||||
"message": "Postojeća veza će biti uklonjena"
|
||||
},
|
||||
"dashboard.sidebar.label.analytics": {
|
||||
"message": "Analitike"
|
||||
},
|
||||
"dashboard.withdraw.error.paypal-country-mismatch.text": {
|
||||
"message": "Molimo te da koristiš odgovarajuću opciju za PayPal transfer za tvoj region (SAD ili međunarodni)."
|
||||
},
|
||||
"dashboard.withdraw.error.paypal-country-mismatch.title": {
|
||||
"message": "Nepoklapanje regiona PayPala"
|
||||
},
|
||||
"discover.install.back-to-server": {
|
||||
"message": "Nazad na server"
|
||||
},
|
||||
@@ -749,6 +995,9 @@
|
||||
"landing.heading.the-place-for-minecraft.servers": {
|
||||
"message": "serveri"
|
||||
},
|
||||
"layout.action.analytics-events": {
|
||||
"message": "Analitički događaji"
|
||||
},
|
||||
"layout.action.change-theme": {
|
||||
"message": "Promeni temu"
|
||||
},
|
||||
@@ -911,6 +1160,69 @@
|
||||
"moderation.page.reports": {
|
||||
"message": "Izvještaji"
|
||||
},
|
||||
"muralpay.country.at": {
|
||||
"message": "Austrija"
|
||||
},
|
||||
"muralpay.country.be": {
|
||||
"message": "Belgija"
|
||||
},
|
||||
"muralpay.country.cy": {
|
||||
"message": "Kipar"
|
||||
},
|
||||
"muralpay.country.de": {
|
||||
"message": "Nemačka"
|
||||
},
|
||||
"muralpay.country.ee": {
|
||||
"message": "Estonija"
|
||||
},
|
||||
"muralpay.country.es": {
|
||||
"message": "Španija"
|
||||
},
|
||||
"muralpay.country.fi": {
|
||||
"message": "Finska"
|
||||
},
|
||||
"muralpay.country.fr": {
|
||||
"message": "Francuska"
|
||||
},
|
||||
"muralpay.country.gr": {
|
||||
"message": "Grčka"
|
||||
},
|
||||
"muralpay.country.ie": {
|
||||
"message": "Irska"
|
||||
},
|
||||
"muralpay.country.it": {
|
||||
"message": "Italija"
|
||||
},
|
||||
"muralpay.country.lt": {
|
||||
"message": "Litvanija"
|
||||
},
|
||||
"muralpay.country.lu": {
|
||||
"message": "Luksemburg"
|
||||
},
|
||||
"muralpay.country.lv": {
|
||||
"message": "Letonija"
|
||||
},
|
||||
"muralpay.country.mt": {
|
||||
"message": "Malta"
|
||||
},
|
||||
"muralpay.country.nl": {
|
||||
"message": "Holandija"
|
||||
},
|
||||
"muralpay.country.pt": {
|
||||
"message": "Portugal"
|
||||
},
|
||||
"muralpay.country.sk": {
|
||||
"message": "Slovačka"
|
||||
},
|
||||
"muralpay.field.country": {
|
||||
"message": "Država"
|
||||
},
|
||||
"muralpay.field.phone-number": {
|
||||
"message": "Broj telefona"
|
||||
},
|
||||
"profile.button.analytics": {
|
||||
"message": "Pregled analitike korisnika"
|
||||
},
|
||||
"profile.button.billing": {
|
||||
"message": "Upravljaj naplatom korisnika"
|
||||
},
|
||||
@@ -1145,6 +1457,9 @@
|
||||
"scopes.analytics.label": {
|
||||
"message": "Pristup analitici"
|
||||
},
|
||||
"scopes.category.analytics": {
|
||||
"message": "Analitike"
|
||||
},
|
||||
"scopes.collectionCreate.description": {
|
||||
"message": "Omogućava kreiranje kolekcija"
|
||||
},
|
||||
@@ -1421,12 +1736,45 @@
|
||||
"settings.account.delete.section.description": {
|
||||
"message": "Kad obrišeš svoj račun, povratka nema. Brisanjem računa sa naših servera biće uklonjeni svi povezani podaci, izuzev projekata."
|
||||
},
|
||||
"settings.applications.head-title": {
|
||||
"message": "Aplikacije"
|
||||
},
|
||||
"settings.applications.modal.header": {
|
||||
"message": "Informacije aplikacije"
|
||||
},
|
||||
"settings.authorizations.about-this-app": {
|
||||
"message": "O ovoj aplikaciji"
|
||||
},
|
||||
"settings.authorizations.by": {
|
||||
"message": "od"
|
||||
},
|
||||
"settings.authorizations.revoke.action": {
|
||||
"message": "Oduzmi"
|
||||
},
|
||||
"settings.billing.charges.product.medal-trial": {
|
||||
"message": "Probna verzija Medal servera"
|
||||
},
|
||||
"settings.billing.charges.product.midas": {
|
||||
"message": "Modrinth Plus"
|
||||
},
|
||||
"settings.billing.interval.month": {
|
||||
"message": "mesec"
|
||||
},
|
||||
"settings.billing.interval.year": {
|
||||
"message": "godina"
|
||||
},
|
||||
"settings.billing.midas.benefits.ad-free": {
|
||||
"message": "Pregledavanje na modrinth.com i Modrinth aplikaciji bez reklama"
|
||||
},
|
||||
"settings.billing.midas.benefits.badge": {
|
||||
"message": "Modrinth+ oznaka na tvom profilu"
|
||||
},
|
||||
"settings.billing.midas.save-per-year": {
|
||||
"message": "Uštedi {amount} godišnje prelaskom na godišnju naplatu!"
|
||||
},
|
||||
"settings.billing.midas.status.cancelled.line1": {
|
||||
"message": "Otkazali smo tvoju pretplatu."
|
||||
},
|
||||
"settings.billing.midas.upsell": {
|
||||
"message": "Postani pretplatnik na Modrinth Plus!"
|
||||
},
|
||||
@@ -1778,12 +2126,18 @@
|
||||
"version.dependency.view-version": {
|
||||
"message": "Vidi verziju"
|
||||
},
|
||||
"version.download.download-dependency": {
|
||||
"message": "Izaberi verziju za instalaciju"
|
||||
},
|
||||
"version.download.optional-resource-pack": {
|
||||
"message": "Opcioni resurs paket"
|
||||
},
|
||||
"version.download.required-resource-pack": {
|
||||
"message": "Potreban paket resursa"
|
||||
},
|
||||
"version.download.version": {
|
||||
"message": "Instaliraj {version} ({size})"
|
||||
},
|
||||
"version.edit.button": {
|
||||
"message": "Promeni"
|
||||
},
|
||||
|
||||
@@ -401,6 +401,9 @@
|
||||
"analytics.threshold.game-versions-above": {
|
||||
"message": "Spelversioner ovan"
|
||||
},
|
||||
"analytics.threshold.project-versions-above": {
|
||||
"message": "Projektversioner ovan"
|
||||
},
|
||||
"analytics.threshold.projects-above": {
|
||||
"message": "Projekt ovan"
|
||||
},
|
||||
@@ -641,11 +644,14 @@
|
||||
"auth.continue-with-provider": {
|
||||
"message": "Fortsätt med {provider}"
|
||||
},
|
||||
"auth.create-account.age-requirement.warning-title": {
|
||||
"message": "Ålderskrav"
|
||||
},
|
||||
"auth.create-account.complete-sign-up": {
|
||||
"message": "Avsluta registrering"
|
||||
},
|
||||
"auth.create-account.date-of-birth.invalid.text": {
|
||||
"message": "Vänligen skriv in ett giltligt födelsedatum. Året kan inte vara 0000."
|
||||
"message": "Vänligen ange ett giltigt födelsedatum. Året kan inte vara 0000."
|
||||
},
|
||||
"auth.create-account.date-of-birth.invalid.title": {
|
||||
"message": "Ogiltligt födelsedatum"
|
||||
@@ -657,11 +663,14 @@
|
||||
"message": "Välj ditt födelsedatum"
|
||||
},
|
||||
"auth.create-account.date-of-birth.required.text": {
|
||||
"message": "Vänligen skriv in ditt födelsedatum innan du fortsätter."
|
||||
"message": "Vänligen ange ditt födelsedatum innan du fortsätter."
|
||||
},
|
||||
"auth.create-account.date-of-birth.required.title": {
|
||||
"message": "Födelsedatum behövs"
|
||||
},
|
||||
"auth.create-account.date-of-birth.under13-helper": {
|
||||
"message": "Du uppfyller inte ålderskravet för att skapa ett konto hos Modrinth."
|
||||
},
|
||||
"auth.create-account.info-panel.source-code-link": {
|
||||
"message": "Relevand källkod"
|
||||
},
|
||||
@@ -671,6 +680,9 @@
|
||||
"auth.create-account.page-title": {
|
||||
"message": "Skapa konto"
|
||||
},
|
||||
"auth.create-account.security-check.label": {
|
||||
"message": "Säkerhetskontroll"
|
||||
},
|
||||
"auth.create-account.subscribe.label": {
|
||||
"message": "Håll mig uppdaterad om de coola sakerna Modrinth arbetar på via e-post"
|
||||
},
|
||||
@@ -687,7 +699,7 @@
|
||||
"message": "Skicka återställningsmejl"
|
||||
},
|
||||
"auth.reset-password.method-choice.description": {
|
||||
"message": "Skriv in din e-post nedan så skickar vi en återställningslänk som låter dig återställa ditt konto."
|
||||
"message": "Ange din e-post nedan så skickar vi en återställningslänk som låter dig återställa ditt konto."
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.text": {
|
||||
"message": "Ett e-postmeddelande med instruktioner har skickats till dig om din e-post tidigare har sparats på ditt konto."
|
||||
@@ -717,7 +729,7 @@
|
||||
"message": "Återställ ditt lösenord"
|
||||
},
|
||||
"auth.sign-in.2fa.description": {
|
||||
"message": "Ange 6-siffriga koden från din autentiseringsapp, eller en av dina reservkoder."
|
||||
"message": "Ange den 6-siffriga koden från din autentiseringsapp eller en av dina reservkoder."
|
||||
},
|
||||
"auth.sign-in.2fa.label": {
|
||||
"message": "Tvåfaktorsautentisering"
|
||||
@@ -1535,9 +1547,18 @@
|
||||
"dashboard.projects.links.header.edit": {
|
||||
"message": "Redigera länkar"
|
||||
},
|
||||
"dashboard.projects.links.placeholder.valid-discord-url": {
|
||||
"message": "Ange en giltig URL för Discordinbjudan"
|
||||
},
|
||||
"dashboard.projects.links.placeholder.valid-url": {
|
||||
"message": "Ange en giltig URL"
|
||||
},
|
||||
"dashboard.projects.links.show-all-projects": {
|
||||
"message": "Visa alla projekt"
|
||||
},
|
||||
"dashboard.projects.links.source-code.description": {
|
||||
"message": "En sida/kodförråd som innehåller källkoden till ditt projekt"
|
||||
},
|
||||
"dashboard.projects.links.source-code.label": {
|
||||
"message": "Källkod"
|
||||
},
|
||||
@@ -1550,6 +1571,18 @@
|
||||
"dashboard.projects.project.icon-alt": {
|
||||
"message": "Ikon för {title}"
|
||||
},
|
||||
"dashboard.projects.project.moderator-message-aria": {
|
||||
"message": "Projektet har ett meddelande från moderatorerna. Se projektet för att se mer."
|
||||
},
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Vänligen verifiera miljömetadata"
|
||||
},
|
||||
"dashboard.projects.selection-bar.aria-label": {
|
||||
"message": "Valda projektåtgärder"
|
||||
},
|
||||
"dashboard.projects.selection-bar.selected-count": {
|
||||
"message": "{count,plural,one{# projekt valt}other{# projekt valda}}"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
@@ -1604,12 +1637,18 @@
|
||||
"dashboard.revenue.transactions.btn.download-csv": {
|
||||
"message": "Ladda ner som CSV"
|
||||
},
|
||||
"dashboard.revenue.transactions.head-title": {
|
||||
"message": "Transaktionshistorik"
|
||||
},
|
||||
"dashboard.revenue.transactions.header": {
|
||||
"message": "Transaktioner"
|
||||
},
|
||||
"dashboard.revenue.transactions.none": {
|
||||
"message": "Inga transaktioner"
|
||||
},
|
||||
"dashboard.revenue.transactions.none.desc": {
|
||||
"message": "Dina utbetalningar och utdrag kommer synas här."
|
||||
},
|
||||
"dashboard.revenue.transactions.period.last-month": {
|
||||
"message": "Senaste månaden"
|
||||
},
|
||||
@@ -1634,6 +1673,9 @@
|
||||
"dashboard.sidebar.label.analytics": {
|
||||
"message": "Statistik"
|
||||
},
|
||||
"dashboard.sidebar.label.creators": {
|
||||
"message": "Skapare"
|
||||
},
|
||||
"dashboard.sidebar.label.dashboard": {
|
||||
"message": "Kontrollpanel"
|
||||
},
|
||||
@@ -1742,6 +1784,9 @@
|
||||
"discover.install.back-to-server": {
|
||||
"message": "Tillbaka till server"
|
||||
},
|
||||
"discover.seo.description": {
|
||||
"message": "Sök och bläddra bland tusen av Minecraft {projectType} på Modrinth med snabba, träffsäkra resultat. Våra filter hjälper dig att snabbt hitta de bästa Minecraft {projectType}."
|
||||
},
|
||||
"discover.seo.title": {
|
||||
"message": "Sök {projectType}"
|
||||
},
|
||||
@@ -1910,6 +1955,9 @@
|
||||
"hosting-marketing.faq.location": {
|
||||
"message": "Var finns Modrinth Hostings servrar? Kan jag välja region?"
|
||||
},
|
||||
"hosting-marketing.faq.location.answer": {
|
||||
"message": "Vi har servrar tillgängliga runtom i Nordamerika, Europa, Sydostasien och Australien för tillfället som du kan välja vid köp. Fler regioner kommer i framtiden! Om du skulle vilja byta din region, vänligen kontakta support."
|
||||
},
|
||||
"hosting-marketing.faq.versions-loaders": {
|
||||
"message": "Vilka Minecraft versioner och loaders kan användas?"
|
||||
},
|
||||
@@ -2459,6 +2507,15 @@
|
||||
"modpack-scan-modal.not-scanned": {
|
||||
"message": "Inte skannad"
|
||||
},
|
||||
"modpack-scan-modal.scan-all-files": {
|
||||
"message": "Skanna alla filer"
|
||||
},
|
||||
"modpack-scan-modal.scan-error": {
|
||||
"message": "Några filer kunde inte skannas: {error}"
|
||||
},
|
||||
"modpack-scan-modal.scanning": {
|
||||
"message": "Skannar..."
|
||||
},
|
||||
"muralpay.account-type.checking": {
|
||||
"message": "Checkkonto"
|
||||
},
|
||||
@@ -2534,6 +2591,12 @@
|
||||
"muralpay.field.account-number": {
|
||||
"message": "Kontonummer"
|
||||
},
|
||||
"muralpay.field.account-number-cbu-cvu": {
|
||||
"message": "Kontonummer (CBU/CVU)"
|
||||
},
|
||||
"muralpay.field.account-number-cci": {
|
||||
"message": "Kontonummer (CCI)"
|
||||
},
|
||||
"muralpay.field.account-number-type": {
|
||||
"message": "Kontonummerstyp"
|
||||
},
|
||||
@@ -2570,8 +2633,11 @@
|
||||
"muralpay.pix-type.phone": {
|
||||
"message": "Telefonnummer"
|
||||
},
|
||||
"muralpay.placeholder.account-number": {
|
||||
"message": "Ange kontonummer"
|
||||
},
|
||||
"muralpay.placeholder.enter-account-number": {
|
||||
"message": "Skriv in kontonummer"
|
||||
"message": "Ange kontonummer"
|
||||
},
|
||||
"muralpay.placeholder.pix-phone": {
|
||||
"message": "+55..."
|
||||
@@ -2609,9 +2675,6 @@
|
||||
"muralpay.rail.fiat-zar.name": {
|
||||
"message": "Banköverföring (ZAR)"
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Ändringar kommer tillämpas på {count} projekt."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "En Modrinth kreatör."
|
||||
},
|
||||
@@ -2672,9 +2735,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Samling"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {nedladdning} other {nedladdningar}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Gick med"
|
||||
},
|
||||
@@ -2912,6 +2972,9 @@
|
||||
"project.download.title": {
|
||||
"message": "Ladda ner {title}"
|
||||
},
|
||||
"project.download.zip-failed-title": {
|
||||
"message": "Kunde inte ladda ner filer"
|
||||
},
|
||||
"project.environment.migration-no-permission.message": {
|
||||
"message": "Vi har förbättrat miljösystemet på Modrinth och nya inställningar är tillgängliga. Du har inte behörighet att ändra dessa inställningar, men meddela en annan medlem i projektet att miljömetadatan måste verifieras."
|
||||
},
|
||||
@@ -3014,6 +3077,9 @@
|
||||
"project.settings.general.url.title": {
|
||||
"message": "URL"
|
||||
},
|
||||
"project.settings.permissions.attention-needed.title": {
|
||||
"message": "Okänt externt innehåll"
|
||||
},
|
||||
"project.settings.permissions.learn-more": {
|
||||
"message": "Läs mer"
|
||||
},
|
||||
@@ -3516,10 +3582,10 @@
|
||||
"message": "Koden du angav är inkorrekt!"
|
||||
},
|
||||
"settings.account.two-factor.field.code.description": {
|
||||
"message": "Ange din tvåfaktorskod för att fortsätta."
|
||||
"message": "Vänligen ange din tvåfaktorskod för att fortsätta."
|
||||
},
|
||||
"settings.account.two-factor.field.code.label": {
|
||||
"message": "Ange en tvåfaktorskod"
|
||||
"message": "Ange tvåfaktorskod"
|
||||
},
|
||||
"settings.account.two-factor.field.code.placeholder": {
|
||||
"message": "Ange kod..."
|
||||
|
||||
@@ -2681,6 +2681,9 @@
|
||||
"moderation.page.external-projects": {
|
||||
"message": "Harici projeler"
|
||||
},
|
||||
"moderation.page.global-detail-traces": {
|
||||
"message": "Global izler"
|
||||
},
|
||||
"moderation.page.projects": {
|
||||
"message": "Projeler"
|
||||
},
|
||||
@@ -2996,9 +2999,6 @@
|
||||
"muralpay.warning.wallet-address": {
|
||||
"message": "Cüzdan adresinizi iki kez kontrol edin. Yanlış adrese gönderilen fonlar kurtarılamaz."
|
||||
},
|
||||
"organization.settings.projects.edit-links.affected-projects": {
|
||||
"message": "Değişiklikler {count, plural, one {# project} other {# projects}} için uygulanacaktır."
|
||||
},
|
||||
"profile.bio.fallback.creator": {
|
||||
"message": "Bir Modrinth içerik üreticisi."
|
||||
},
|
||||
@@ -3065,9 +3065,6 @@
|
||||
"profile.label.collection": {
|
||||
"message": "Koleksiyon"
|
||||
},
|
||||
"profile.label.downloads": {
|
||||
"message": "{count} {countPlural, plural, one {indirme} other {indirmeler}}"
|
||||
},
|
||||
"profile.label.joined": {
|
||||
"message": "Katılma:"
|
||||
},
|
||||
@@ -3086,9 +3083,6 @@
|
||||
"profile.label.organizations": {
|
||||
"message": "Organizasyonlar"
|
||||
},
|
||||
"profile.label.projects": {
|
||||
"message": "{count} {countPlural, plural, one {proje} other {projeler}}"
|
||||
},
|
||||
"profile.label.saving": {
|
||||
"message": "Kaydediliyor..."
|
||||
},
|
||||
@@ -3281,12 +3275,18 @@
|
||||
"project.download.base-loader-incompatible-tooltip": {
|
||||
"message": "Bu yükleyici bu proje ile uyumsuz."
|
||||
},
|
||||
"project.download.compatible-version-title": {
|
||||
"message": "Uyumlu sürümler"
|
||||
},
|
||||
"project.download.dependencies-title": {
|
||||
"message": "Gerekliler"
|
||||
},
|
||||
"project.download.dependency-already-installed": {
|
||||
"message": "Gerekli proje zaten indirilmiş"
|
||||
},
|
||||
"project.download.dependency-any-compatible": {
|
||||
"message": "Herhangi uyumlu"
|
||||
},
|
||||
"project.download.dependency-conflicting": {
|
||||
"message": "Bu gerekli proje başka bir gerekli proje ile çatışıyor"
|
||||
},
|
||||
@@ -3308,12 +3308,30 @@
|
||||
"project.download.dependency-quilt-fabric-api": {
|
||||
"message": "Quilt'te Fabric API'ye gerek yok"
|
||||
},
|
||||
"project.download.dependency-resource-pack-admonition": {
|
||||
"message": "Bu proje bir kaynak paketine bağımlıdır. Kaynak paketini indirin ve {folder} klasörüne yerleştirin."
|
||||
},
|
||||
"project.download.dependency-unavailable": {
|
||||
"message": "Bu bağımlılık indirilemez"
|
||||
},
|
||||
"project.download.download": {
|
||||
"message": "İndir"
|
||||
},
|
||||
"project.download.download-as-zip": {
|
||||
"message": ".zip olarak indir"
|
||||
},
|
||||
"project.download.download-version": {
|
||||
"message": "İndir {version}"
|
||||
},
|
||||
"project.download.download-with-dependencies": {
|
||||
"message": "Bağımlı olanlarla indir"
|
||||
},
|
||||
"project.download.download-with-recommended": {
|
||||
"message": "Tavsiye edilenle indir"
|
||||
},
|
||||
"project.download.download-with-recommended-as-zip": {
|
||||
"message": "Tavsiye edilenle .zip olarak indir"
|
||||
},
|
||||
"project.download.duplicate-dependencies-hidden": {
|
||||
"message": "Yinelenen bağımlılıklar gizlendi."
|
||||
},
|
||||
@@ -3323,6 +3341,9 @@
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title}, {platform} için {gameVersion} sürümünü desteklemiyor"
|
||||
},
|
||||
"project.download.install-with-app": {
|
||||
"message": "Modrinth App'le indir"
|
||||
},
|
||||
"project.download.manually": {
|
||||
"message": "Manuel Yükle"
|
||||
},
|
||||
@@ -3332,9 +3353,21 @@
|
||||
"project.download.no-game-versions-found": {
|
||||
"message": "Oyun sürümleri bulunamadı"
|
||||
},
|
||||
"project.download.no-versions-available": {
|
||||
"message": "{gameVersion} ve {platform} için uyumlu sürüm yok."
|
||||
},
|
||||
"project.download.platform-unsupported-tooltip": {
|
||||
"message": "{title}, {gameVersion} sürümünde {platform} desteklemiyor"
|
||||
},
|
||||
"project.download.recommended-title": {
|
||||
"message": "Önerilen"
|
||||
},
|
||||
"project.download.required-resource-pack-admonition": {
|
||||
"message": "Bu data paketi bir kaynak paketine bağımlıdır. Kaynak paketini indirin ve {folder} klasörüne yerleştirin."
|
||||
},
|
||||
"project.download.required-resource-pack-short": {
|
||||
"message": "Kaynak paketi"
|
||||
},
|
||||
"project.download.search-game-versions": {
|
||||
"message": "Oyun sürümü seç"
|
||||
},
|
||||
@@ -3353,6 +3386,12 @@
|
||||
"project.download.unknown-loader": {
|
||||
"message": "Bilinmeyen yükleyici"
|
||||
},
|
||||
"project.download.zip-failed-text": {
|
||||
"message": "Bir veya daha fazla dosya indirilemedi. Lütfen tekrar deneyin."
|
||||
},
|
||||
"project.download.zip-failed-title": {
|
||||
"message": "Dosyalar indirilemedi"
|
||||
},
|
||||
"project.environment.migration-no-permission.message": {
|
||||
"message": "Daha yeni Modrinth'in Çevre sistemini yeniledik ve yeni seçenekler mevcut. Bu ayarları değiştirmek için yetkiniz yok ama projenin başka bir üyesine çevre meta verisinin doğrulanması gerektiği haberini verebilirsiniz."
|
||||
},
|
||||
@@ -3554,12 +3593,6 @@
|
||||
"project.settings.visit-dashboard": {
|
||||
"message": "Projeler panelini ziyaret et"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "{count, plural, one {download} other {downloads}}"
|
||||
},
|
||||
"project.stats.followers-label": {
|
||||
"message": "{count, plural, one {follower} other {followers}}"
|
||||
},
|
||||
"project.status.archived.message": {
|
||||
"message": "{title} arşivlenmiş. {title}, yapımcı fikrini değiştirmediği sürece daha fazla güncelleme almayacak."
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user