Compare commits
12 Commits
chore/post
...
v0.9.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4180544e0a | ||
|
|
9168d349fc | ||
|
|
3dad6b317f | ||
|
|
4a2605bc1e | ||
|
|
41543e3af0 | ||
|
|
6003f1a10e | ||
|
|
3d9be0cc3f | ||
|
|
5e7444f115 | ||
|
|
20fcf70e90 | ||
|
|
0508f13cb6 | ||
|
|
2f68c62b3a | ||
|
|
ea64e08791 |
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -2604,6 +2604,28 @@ dependencies = [
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumset"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293"
|
||||
dependencies = [
|
||||
"enumset_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumset_derive"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242"
|
||||
dependencies = [
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
@@ -9236,9 +9258,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
dependencies = [
|
||||
"ariadne",
|
||||
"async-compression",
|
||||
"async-recursion",
|
||||
"async-tungstenite",
|
||||
"async-walkdir",
|
||||
@@ -9253,6 +9276,7 @@ dependencies = [
|
||||
"discord-rich-presence",
|
||||
"dunce",
|
||||
"either",
|
||||
"enumset",
|
||||
"flate2",
|
||||
"fs4",
|
||||
"futures",
|
||||
@@ -9293,13 +9317,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus_gui"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cocoa 0.25.0",
|
||||
"daedalus",
|
||||
"dashmap 6.1.0",
|
||||
"either",
|
||||
"enumset",
|
||||
"native-dialog",
|
||||
"objc",
|
||||
"opener",
|
||||
@@ -9333,6 +9358,7 @@ dependencies = [
|
||||
name = "theseus_playground"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"enumset",
|
||||
"theseus",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useLoading, useTheming } from '@/store/state'
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -15,7 +15,7 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -23,7 +23,7 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -31,7 +31,7 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||
}
|
||||
|
||||
.font-minecraft {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ref, computed } from 'vue'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { cancel_directory_change } from '@/helpers/settings.js'
|
||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||
import { install } from '@/helpers/profile.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalWrapper ref="modal" header="Create instance">
|
||||
<ModalWrapper ref="modal" header="Creating an instance">
|
||||
<div class="modal-header">
|
||||
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
|
||||
</div>
|
||||
|
||||
@@ -124,8 +124,11 @@ import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { ref, computed } from 'vue'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
@@ -168,6 +171,9 @@ async function install() {
|
||||
installing.value = false
|
||||
emit('install', props.project.project_id ?? props.project.id)
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,15 +13,17 @@ const confirmModal = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
const onCreateInstance = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: (projectVal, versionIdVal, callback) => {
|
||||
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
||||
project.value = projectVal
|
||||
versionId.value = versionIdVal
|
||||
installing.value = false
|
||||
confirmModal.value.show()
|
||||
|
||||
onInstall.value = callback
|
||||
onCreateInstance.value = createInstanceCallback
|
||||
|
||||
trackEvent('PackInstallStart')
|
||||
},
|
||||
@@ -36,6 +38,7 @@ async function install() {
|
||||
versionId.value,
|
||||
project.value.title,
|
||||
project.value.icon_url,
|
||||
onCreateInstance.value,
|
||||
).catch(handleError)
|
||||
trackEvent('PackInstall', {
|
||||
id: project.value.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Checkbox } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Checkbox, Toggle } from '@modrinth/ui'
|
||||
import { computed, ref, type Ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/
|
||||
import { useTheming } from '@/store/state'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { NewModal as Modal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ShareModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
import type { ColorTheme } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -24,13 +25,13 @@ watch(
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
(theme) => {
|
||||
(theme: ColorTheme) => {
|
||||
themeStore.setThemeState(theme)
|
||||
settings.theme = theme
|
||||
}
|
||||
"
|
||||
:current-theme="settings.theme"
|
||||
:theme-options="themeStore.themeOptions"
|
||||
:theme-options="themeStore.getThemeOptions()"
|
||||
system-theme-color="system"
|
||||
/>
|
||||
|
||||
@@ -80,10 +81,28 @@ watch(
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="w-40"
|
||||
:options="['Home', 'Library']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
||||
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
||||
themeStore.featureFlags['worlds_in_home'] = newValue
|
||||
settings.feature_flags['worlds_in_home'] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { handleError } from '@/store/notifications'
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const settings = ref(await get())
|
||||
const options = ref(['project_background', 'page_path', 'worlds_tab'])
|
||||
const settings = ref(await getSettings())
|
||||
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
||||
|
||||
function getStoreValue(key: string) {
|
||||
return themeStore.featureFlags[key] ?? false
|
||||
}
|
||||
|
||||
function setStoreValue(key: string, value: boolean) {
|
||||
function setFeatureFlag(key: string, value: boolean) {
|
||||
themeStore.featureFlags[key] = value
|
||||
settings.value.feature_flags[key] = value
|
||||
}
|
||||
@@ -21,7 +18,7 @@ function setStoreValue(key: string, value: boolean) {
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
await setSettings(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@@ -36,8 +33,8 @@ watch(
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="getStoreValue(option)"
|
||||
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
|
||||
:model-value="themeStore.getFeatureFlag(option)"
|
||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { Button, Slider } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
|
||||
@@ -153,7 +153,7 @@ onUnmounted(() => {
|
||||
•
|
||||
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
||||
<router-link
|
||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary"
|
||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||
:to="`/project/${modpack.id}`"
|
||||
>
|
||||
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
||||
|
||||
@@ -13,16 +13,17 @@ import {
|
||||
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||
import { watch, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch, onMounted, onUnmounted, ref, computed } from 'vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { kill } from '@/helpers/profile'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { get_all } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
|
||||
const props = defineProps<{
|
||||
recentInstances: GameInstance[]
|
||||
@@ -54,7 +55,9 @@ type WorldJumpBackInItem = BaseJumpBackInItem & {
|
||||
|
||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
||||
|
||||
watch(props.recentInstances, async () => {
|
||||
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
||||
|
||||
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||
await populateJumpBackIn().catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
@@ -66,66 +69,71 @@ await populateJumpBackIn().catch(() => {
|
||||
|
||||
async function populateJumpBackIn() {
|
||||
console.info('Repopulating jump back in...')
|
||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN)
|
||||
|
||||
const worldItems: WorldJumpBackInItem[] = []
|
||||
worlds.forEach((world) => {
|
||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||
|
||||
if (!instance || !world.last_played) {
|
||||
return
|
||||
}
|
||||
if (showWorlds.value) {
|
||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
||||
|
||||
worldItems.push({
|
||||
type: 'world',
|
||||
last_played: dayjs(world.last_played),
|
||||
world: world,
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
worlds.forEach((world) => {
|
||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||
|
||||
const servers: {
|
||||
instancePath: string
|
||||
address: string
|
||||
}[] = worldItems
|
||||
.filter((item) => item.world.type === 'server' && item.instance)
|
||||
.map((item) => ({
|
||||
instancePath: item.instance.path,
|
||||
address: (item.world as ServerWorld).address,
|
||||
}))
|
||||
|
||||
// fetch protocol versions for all unique MC versions with server worlds
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||
await Promise.all(
|
||||
[...uniqueServerInstances].map((path) => {
|
||||
get_profile_protocol_version(path)
|
||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||
.catch(() => {
|
||||
console.error(`Failed to get profile protocol for: ${path} `)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// initialize server data
|
||||
servers.forEach(({ address }) => {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
if (!instance || !world.last_played) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
await Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
)
|
||||
worldItems.push({
|
||||
type: 'world',
|
||||
last_played: dayjs(world.last_played),
|
||||
world: world,
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
|
||||
const servers: {
|
||||
instancePath: string
|
||||
address: string
|
||||
}[] = worldItems
|
||||
.filter((item) => item.world.type === 'server' && item.instance)
|
||||
.map((item) => ({
|
||||
instancePath: item.instance.path,
|
||||
address: (item.world as ServerWorld).address,
|
||||
}))
|
||||
|
||||
// fetch protocol versions for all unique MC versions with server worlds
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||
await Promise.all(
|
||||
[...uniqueServerInstances].map((path) =>
|
||||
get_profile_protocol_version(path)
|
||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||
.catch(() => {
|
||||
console.error(`Failed to get profile protocol for: ${path} `)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// initialize server data
|
||||
servers.forEach(({ address }) => {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const instanceItems: InstanceJumpBackInItem[] = []
|
||||
props.recentInstances.forEach((instance) => {
|
||||
if (worldItems.some((item) => item.instance.path === instance.path) || !instance.last_played) {
|
||||
return
|
||||
for (const instance of props.recentInstances) {
|
||||
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
||||
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
||||
continue
|
||||
}
|
||||
|
||||
instanceItems.push({
|
||||
@@ -133,13 +141,13 @@ async function populateJumpBackIn() {
|
||||
last_played: dayjs(instance.last_played),
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
||||
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
|
||||
jumpBackInItems.value = items.filter(
|
||||
(item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO),
|
||||
)
|
||||
jumpBackInItems.value = items
|
||||
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
||||
.slice(0, MAX_JUMP_BACK_IN)
|
||||
}
|
||||
|
||||
async function refreshServer(address: string, instancePath: string) {
|
||||
@@ -155,6 +163,18 @@ async function joinWorld(world: WorldWithProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
async function playInstance(instance: GameInstance) {
|
||||
await run(instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
loader: instance.loader,
|
||||
game_version: instance.game_version,
|
||||
source: 'WorldItem',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function stopInstance(path: string) {
|
||||
await kill(path).catch(handleError)
|
||||
trackEvent('InstanceStop', {
|
||||
@@ -209,11 +229,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||
<HeadingLink
|
||||
v-if="(theme.featureFlags as Record<string, boolean>)['worlds_tab']"
|
||||
to="/worlds"
|
||||
class="mt-1"
|
||||
>
|
||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||
Jump back in
|
||||
</HeadingLink>
|
||||
<span
|
||||
@@ -222,7 +238,7 @@ onUnmounted(() => {
|
||||
>
|
||||
Jump back in
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||
<template
|
||||
v-for="item in jumpBackInItems"
|
||||
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||
@@ -246,7 +262,7 @@ onUnmounted(() => {
|
||||
:rendered-motd="
|
||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||
"
|
||||
:current-protocol="protocolVersions[item.instance.game_version]"
|
||||
:current-protocol="protocolVersions[item.instance.path]"
|
||||
:game-mode="
|
||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||
"
|
||||
@@ -259,6 +275,7 @@ onUnmounted(() => {
|
||||
? refreshServer(item.world.address, item.instance.path)
|
||||
: {}
|
||||
"
|
||||
@update="() => populateJumpBackIn()"
|
||||
@play="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
@@ -266,6 +283,12 @@ onUnmounted(() => {
|
||||
joinWorld(item.world)
|
||||
}
|
||||
"
|
||||
@play-instance="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
playInstance(item.instance)
|
||||
}
|
||||
"
|
||||
@stop="() => stopInstance(item.instance.path)"
|
||||
/>
|
||||
<InstanceItem v-else :instance="item.instance" />
|
||||
@@ -273,3 +296,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.grid-when-huge {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
||||
import { getWorldIdentifier, showWorldInFolder } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
set_world_display_status,
|
||||
getWorldIdentifier,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import {
|
||||
IssuesIcon,
|
||||
@@ -19,6 +23,7 @@ import {
|
||||
TrashIcon,
|
||||
UpdatedIcon,
|
||||
UserIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
|
||||
import type { MessageDescriptor } from '@vintl/vintl'
|
||||
@@ -35,7 +40,7 @@ const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'play' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -69,6 +74,7 @@ const props = withDefaults(
|
||||
playingWorld: false,
|
||||
startingInstance: false,
|
||||
supportsQuickPlay: false,
|
||||
currentProtocol: null,
|
||||
|
||||
refreshing: false,
|
||||
serverStatus: undefined,
|
||||
@@ -143,10 +149,18 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.play_anyway',
|
||||
defaultMessage: 'Play anyway',
|
||||
},
|
||||
playInstance: {
|
||||
id: 'instance.worlds.play_instance',
|
||||
defaultMessage: 'Play instance',
|
||||
},
|
||||
worldInUse: {
|
||||
id: 'instance.worlds.world_in_use',
|
||||
defaultMessage: 'World is in use',
|
||||
},
|
||||
dontShowOnHome: {
|
||||
id: 'instance.worlds.dont_show_on_home',
|
||||
defaultMessage: `Don't show on Home`,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
@@ -336,6 +350,12 @@ const messages = defineMessages({
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
shown: !!instancePath,
|
||||
disabled: playingInstance,
|
||||
action: () => emit('play-instance'),
|
||||
},
|
||||
{
|
||||
id: 'play-anyway',
|
||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
||||
@@ -344,7 +364,7 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instancePath,
|
||||
action: () => router.push(encodeURI(`/instance/${instancePath}/worlds`)),
|
||||
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
@@ -369,6 +389,24 @@ const messages = defineMessages({
|
||||
action: () =>
|
||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !!instancePath,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
shown: !!instancePath,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instancePath,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
).then(() => {
|
||||
emit('update')
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !instancePath,
|
||||
@@ -385,6 +423,10 @@ const messages = defineMessages({
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #play-instance>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playInstance) }}
|
||||
</template>
|
||||
<template #play-anyway>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playAnyway) }}
|
||||
@@ -406,6 +448,10 @@ const messages = defineMessages({
|
||||
<template #refresh>
|
||||
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { edit_server_in_profile, type ServerWorld } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
type ServerPackStatus,
|
||||
edit_server_in_profile,
|
||||
type ServerWorld,
|
||||
set_world_display_status,
|
||||
type DisplayStatus,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -21,10 +28,14 @@ const props = defineProps<{
|
||||
|
||||
const modal = ref()
|
||||
|
||||
const name = ref()
|
||||
const address = ref()
|
||||
const resourcePack = ref('enabled')
|
||||
const index = ref()
|
||||
const name = ref<string>('')
|
||||
const address = ref<string>('')
|
||||
const resourcePack = ref<ServerPackStatus>('enabled')
|
||||
const index = ref<number>(0)
|
||||
const displayStatus = ref<DisplayStatus>('normal')
|
||||
const hideFromHome = ref(false)
|
||||
|
||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||
|
||||
async function saveServer() {
|
||||
const serverName = name.value ? name.value : address.value
|
||||
@@ -36,12 +47,23 @@ async function saveServer() {
|
||||
address.value,
|
||||
resourcePackStatus,
|
||||
).catch(handleError)
|
||||
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
'server',
|
||||
address.value,
|
||||
newDisplayStatus.value,
|
||||
)
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
name: serverName,
|
||||
type: 'server',
|
||||
index: index.value,
|
||||
address: address.value,
|
||||
pack_status: resourcePackStatus,
|
||||
display_status: newDisplayStatus.value,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
@@ -51,6 +73,8 @@ function show(server: ServerWorld) {
|
||||
address.value = server.address
|
||||
resourcePack.value = server.pack_status
|
||||
index.value = server.index
|
||||
displayStatus.value = server.display_status
|
||||
hideFromHome.value = server.display_status === 'hidden'
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
@@ -75,6 +99,7 @@ const titleMessage = defineMessage({
|
||||
v-model:address="address"
|
||||
v-model:resource-pack="resourcePack"
|
||||
/>
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!address" @click="saveServer">
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import type { SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||
import { rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
||||
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||
import { set_world_display_status, rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [path: string, name: string, removeIcon: boolean]
|
||||
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -25,6 +26,10 @@ const icon = ref()
|
||||
const name = ref()
|
||||
const path = ref()
|
||||
const removeIcon = ref(false)
|
||||
const displayStatus = ref<DisplayStatus>('normal')
|
||||
const hideFromHome = ref(false)
|
||||
|
||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||
|
||||
async function saveWorld() {
|
||||
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
||||
@@ -32,8 +37,16 @@ async function saveWorld() {
|
||||
if (removeIcon.value) {
|
||||
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
||||
}
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
'singleplayer',
|
||||
path.value,
|
||||
newDisplayStatus.value,
|
||||
)
|
||||
}
|
||||
|
||||
emit('submit', path.value, name.value, removeIcon.value)
|
||||
emit('submit', path.value, name.value, removeIcon.value, newDisplayStatus.value)
|
||||
hide()
|
||||
}
|
||||
|
||||
@@ -41,6 +54,8 @@ function show(world: SingleplayerWorld) {
|
||||
name.value = world.name
|
||||
path.value = world.path
|
||||
icon.value = world.icon
|
||||
displayStatus.value = world.display_status
|
||||
hideFromHome.value = world.display_status === 'hidden'
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
@@ -87,6 +102,7 @@ const messages = defineMessages({
|
||||
class="w-full"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { computed } from 'vue'
|
||||
import { Checkbox } from '@modrinth/ui'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const value = defineModel<boolean>({ required: true })
|
||||
|
||||
const labelMessage = defineMessage({
|
||||
id: 'instance.edit-world.hide-from-home',
|
||||
defaultMessage: `Hide from the Home page`,
|
||||
})
|
||||
|
||||
const label = computed(() => formatMessage(labelMessage))
|
||||
</script>
|
||||
<template>
|
||||
<Checkbox v-model="value" :label="label" />
|
||||
</template>
|
||||
@@ -7,7 +7,13 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { create } from './profile'
|
||||
|
||||
// Installs pack from a version ID
|
||||
export async function create_profile_and_install(projectId, versionId, packTitle, iconUrl) {
|
||||
export async function create_profile_and_install(
|
||||
projectId,
|
||||
versionId,
|
||||
packTitle,
|
||||
iconUrl,
|
||||
createInstanceCallback = () => {},
|
||||
) {
|
||||
const location = {
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
@@ -24,6 +30,7 @@ export async function create_profile_and_install(projectId, versionId, packTitle
|
||||
null,
|
||||
true,
|
||||
)
|
||||
createInstanceCallback(profile)
|
||||
|
||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
// Settings object
|
||||
/*
|
||||
|
||||
Settings {
|
||||
"memory": MemorySettings,
|
||||
"game_resolution": [int int],
|
||||
"custom_java_args": [String ...],
|
||||
"custom_env_args" : [(string, string) ... ]>,
|
||||
"java_globals": Hash of (string, Path),
|
||||
"default_user": Uuid string (can be null),
|
||||
"hooks": Hooks,
|
||||
"max_concurrent_downloads": uint,
|
||||
"version": u32,
|
||||
"collapsed_navigation": bool,
|
||||
}
|
||||
|
||||
Memorysettings {
|
||||
"min": u32, can be null,
|
||||
"max": u32,
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// Get full settings object
|
||||
export async function get() {
|
||||
return await invoke('plugin:settings|settings_get')
|
||||
}
|
||||
|
||||
// Set full settings object
|
||||
export async function set(settings) {
|
||||
return await invoke('plugin:settings|settings_set', { settings })
|
||||
}
|
||||
|
||||
export async function cancel_directory_change() {
|
||||
return await invoke('plugin:settings|cancel_directory_change')
|
||||
}
|
||||
78
apps/app-frontend/src/helpers/settings.ts
Normal file
78
apps/app-frontend/src/helpers/settings.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
||||
|
||||
// Settings object
|
||||
/*
|
||||
|
||||
Settings {
|
||||
"memory": MemorySettings,
|
||||
"game_resolution": [int int],
|
||||
"custom_java_args": [String ...],
|
||||
"custom_env_args" : [(string, string) ... ]>,
|
||||
"java_globals": Hash of (string, Path),
|
||||
"default_user": Uuid string (can be null),
|
||||
"hooks": Hooks,
|
||||
"max_concurrent_downloads": uint,
|
||||
"version": u32,
|
||||
"collapsed_navigation": bool,
|
||||
}
|
||||
|
||||
Memorysettings {
|
||||
"min": u32, can be null,
|
||||
"max": u32,
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
export type AppSettings = {
|
||||
max_concurrent_downloads: number
|
||||
max_concurrent_writes: number
|
||||
|
||||
theme: ColorTheme
|
||||
default_page: 'home' | 'library'
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
toggle_sidebar: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
personalized_ads: boolean
|
||||
|
||||
onboarded: boolean
|
||||
|
||||
extra_launch_args: string[]
|
||||
custom_env_vars: [string, string][]
|
||||
memory: MemorySettings
|
||||
force_fullscreen: boolean
|
||||
game_resolution: WindowSize
|
||||
hide_on_process_start: boolean
|
||||
hooks: Hooks
|
||||
|
||||
custom_dir?: string | null
|
||||
prev_custom_dir?: string | null
|
||||
migrated: boolean
|
||||
|
||||
developer_mode: boolean
|
||||
feature_flags: Record<FeatureFlag, boolean>
|
||||
}
|
||||
|
||||
// Get full settings object
|
||||
export async function get() {
|
||||
return (await invoke('plugin:settings|settings_get')) as AppSettings
|
||||
}
|
||||
|
||||
// Set full settings object
|
||||
export async function set(settings: AppSettings) {
|
||||
return await invoke('plugin:settings|settings_set', { settings })
|
||||
}
|
||||
|
||||
export async function cancel_directory_change(): Promise<void> {
|
||||
return await invoke('plugin:settings|cancel_directory_change')
|
||||
}
|
||||
27
apps/app-frontend/src/helpers/types.d.ts
vendored
27
apps/app-frontend/src/helpers/types.d.ts
vendored
@@ -48,6 +48,32 @@ type LinkedData = {
|
||||
|
||||
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||
|
||||
type ContentFile = {
|
||||
hash: string
|
||||
file_name: string
|
||||
size: number
|
||||
metadata?: FileMetadata
|
||||
update_version_id?: string
|
||||
project_type: ContentFileProjectType
|
||||
}
|
||||
|
||||
type FileMetadata = {
|
||||
project_id: string
|
||||
version_id: string
|
||||
}
|
||||
|
||||
type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
|
||||
|
||||
type CacheBehaviour =
|
||||
// Serve expired data. If fetch fails / launcher is offline, errors are ignored
|
||||
| 'stale_while_revalidate_skip_offline'
|
||||
// Serve expired data, revalidate in background
|
||||
| 'stale_while_revalidate'
|
||||
// Must revalidate if data is expired
|
||||
| 'must_revalidate'
|
||||
// Ignore cache- always fetch updated data from origin
|
||||
| 'bypass'
|
||||
|
||||
type MemorySettings = {
|
||||
maximum: number
|
||||
}
|
||||
@@ -88,6 +114,7 @@ type AppSettings = {
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
worlds_in_home: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
|
||||
@@ -9,8 +9,13 @@ type BaseWorld = {
|
||||
name: string
|
||||
last_played?: string
|
||||
icon?: string
|
||||
display_status: DisplayStatus
|
||||
type: WorldType
|
||||
}
|
||||
|
||||
export type WorldType = 'singleplayer' | 'server'
|
||||
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
|
||||
|
||||
export type SingleplayerWorld = BaseWorld & {
|
||||
type: 'singleplayer'
|
||||
path: string
|
||||
@@ -70,8 +75,11 @@ export type ServerData = {
|
||||
renderedMotd?: string
|
||||
}
|
||||
|
||||
export async function get_recent_worlds(limit: number): Promise<WorldWithProfile[]> {
|
||||
return await invoke('plugin:worlds|get_recent_worlds', { limit })
|
||||
export async function get_recent_worlds(
|
||||
limit: number,
|
||||
displayStatuses?: DisplayStatus[],
|
||||
): Promise<WorldWithProfile[]> {
|
||||
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
|
||||
}
|
||||
|
||||
export async function get_profile_worlds(path: string): Promise<World[]> {
|
||||
@@ -85,6 +93,20 @@ export async function get_singleplayer_world(
|
||||
return await invoke('plugin:worlds|get_singleplayer_world', { instance, world })
|
||||
}
|
||||
|
||||
export async function set_world_display_status(
|
||||
instance: string,
|
||||
worldType: WorldType,
|
||||
worldId: string,
|
||||
displayStatus: DisplayStatus,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:worlds|set_world_display_status', {
|
||||
instance,
|
||||
worldType,
|
||||
worldId,
|
||||
displayStatus,
|
||||
})
|
||||
}
|
||||
|
||||
export async function rename_world(
|
||||
instance: string,
|
||||
world: string,
|
||||
@@ -230,12 +252,14 @@ export async function refreshServers(
|
||||
|
||||
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
||||
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
||||
const newWorld = await get_singleplayer_world(instancePath, worldPath)
|
||||
if (index !== -1) {
|
||||
worlds[index] = await get_singleplayer_world(instancePath, worldPath)
|
||||
sortWorlds(worlds)
|
||||
worlds[index] = newWorld
|
||||
} else {
|
||||
console.error(`Error refreshing world, could not find world at path ${worldPath}.`)
|
||||
console.info(`Adding new world at path: ${worldPath}.`)
|
||||
worlds.push(newWorld)
|
||||
}
|
||||
sortWorlds(worlds)
|
||||
}
|
||||
|
||||
export async function handleDefaultProfileUpdateEvent(
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||
<button
|
||||
v-for="filter in filterOptions"
|
||||
:key="filter"
|
||||
:key="`content-filter-${filter.id}`"
|
||||
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
||||
@click="toggleArray(selectedFilters, filter.id)"
|
||||
>
|
||||
@@ -47,7 +47,7 @@
|
||||
path: x.path,
|
||||
disabled: x.disabled,
|
||||
filename: x.file_name,
|
||||
icon: x.icon,
|
||||
icon: x.icon ?? undefined,
|
||||
title: x.name,
|
||||
data: x,
|
||||
}
|
||||
@@ -156,7 +156,7 @@
|
||||
color-fill="text"
|
||||
hover-color-fill="text"
|
||||
>
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal.show()">
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal?.show()">
|
||||
<DownloadIcon /> Update pack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -170,7 +170,7 @@
|
||||
>
|
||||
<button
|
||||
v-tooltip="`Update`"
|
||||
:disabled="(item.data as any).updating"
|
||||
:disabled="(item.data as ProjectListEntry).updating"
|
||||
@click="updateProject(item.data)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
@@ -276,6 +276,7 @@ import {
|
||||
RadialHeader,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
@@ -305,43 +306,18 @@ import { profile_listener } from '@/helpers/events.js'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import dayjs from 'dayjs'
|
||||
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
offline: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
playing: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
options: InstanceType<typeof ContextMenu>
|
||||
offline: boolean
|
||||
playing: boolean
|
||||
versions: Version[]
|
||||
installed: boolean
|
||||
}>()
|
||||
|
||||
type ProjectListEntryAuthor = {
|
||||
name: string
|
||||
@@ -356,13 +332,15 @@ type ProjectListEntry = {
|
||||
author: ProjectListEntryAuthor | null
|
||||
version: string | null
|
||||
file_name: string
|
||||
icon: string | null
|
||||
icon: string | undefined
|
||||
disabled: boolean
|
||||
updateVersion?: string
|
||||
outdated: boolean
|
||||
updated: dayjs.Dayjs
|
||||
project_type: string
|
||||
id?: string
|
||||
updating?: boolean
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const isPackLocked = computed(() => {
|
||||
@@ -375,17 +353,20 @@ const canUpdatePack = computed(() => {
|
||||
const exportModal = ref(null)
|
||||
|
||||
const projects = ref<ProjectListEntry[]>([])
|
||||
const selectedFiles = ref([])
|
||||
const selectedFiles = ref<string[]>([])
|
||||
const selectedProjects = computed(() =>
|
||||
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
|
||||
)
|
||||
|
||||
const selectionMap = ref(new Map())
|
||||
|
||||
const initProjects = async (cacheBehaviour?) => {
|
||||
const initProjects = async (cacheBehaviour?: CacheBehaviour) => {
|
||||
const newProjects: ProjectListEntry[] = []
|
||||
|
||||
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
|
||||
const profileProjects = (await get_projects(props.instance.path, cacheBehaviour)) as Record<
|
||||
string,
|
||||
ContentFile
|
||||
>
|
||||
const fetchProjects = []
|
||||
const fetchVersions = []
|
||||
|
||||
@@ -397,21 +378,21 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
|
||||
const [modrinthProjects, modrinthVersions] = await Promise.all([
|
||||
await get_project_many(fetchProjects).catch(handleError),
|
||||
await get_version_many(fetchVersions).catch(handleError),
|
||||
(await get_project_many(fetchProjects).catch(handleError)) as Project[],
|
||||
(await get_version_many(fetchVersions).catch(handleError)) as Version[],
|
||||
])
|
||||
|
||||
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
|
||||
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError),
|
||||
await get_organization_many(
|
||||
(await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError)) as TeamMember[][],
|
||||
(await get_organization_many(
|
||||
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
|
||||
).catch(handleError),
|
||||
).catch(handleError)) as Organization[],
|
||||
])
|
||||
|
||||
for (const [path, file] of Object.entries(profileProjects)) {
|
||||
if (file.metadata) {
|
||||
const project = modrinthProjects.find((x) => file.metadata.project_id === x.id)
|
||||
const version = modrinthVersions.find((x) => file.metadata.version_id === x.id)
|
||||
const project = modrinthProjects.find((x) => file.metadata?.project_id === x.id)
|
||||
const version = modrinthVersions.find((x) => file.metadata?.version_id === x.id)
|
||||
|
||||
if (project && version) {
|
||||
const org = project.organization
|
||||
@@ -420,7 +401,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
|
||||
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
|
||||
|
||||
let author: ProjectListEntryAuthor | null
|
||||
let author: ProjectListEntryAuthor | null = null
|
||||
if (org) {
|
||||
author = {
|
||||
name: org.name,
|
||||
@@ -429,13 +410,13 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
} else if (team) {
|
||||
const teamMember = team.find((x) => x.is_owner)
|
||||
author = {
|
||||
name: teamMember.user.username,
|
||||
slug: teamMember.user.username,
|
||||
type: 'user',
|
||||
if (teamMember) {
|
||||
author = {
|
||||
name: teamMember.user.username,
|
||||
slug: teamMember.user.username,
|
||||
type: 'user',
|
||||
}
|
||||
}
|
||||
} else {
|
||||
author = null
|
||||
}
|
||||
|
||||
newProjects.push({
|
||||
@@ -464,7 +445,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
author: null,
|
||||
version: null,
|
||||
file_name: file.file_name,
|
||||
icon: null,
|
||||
icon: undefined,
|
||||
disabled: file.file_name.endsWith('.disabled'),
|
||||
outdated: false,
|
||||
updated: dayjs(0),
|
||||
@@ -488,7 +469,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
await initProjects()
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
const modpackVersionModal = ref<InstanceType<typeof ModpackVersionModal> | null>()
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const vintl = useVIntl()
|
||||
@@ -513,7 +494,7 @@ const messages = defineMessages({
|
||||
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
const options: FilterOption[] = []
|
||||
|
||||
const frequency = projects.value.reduce((map, item) => {
|
||||
const frequency = projects.value.reduce((map: Record<string, number>, item) => {
|
||||
map[item.project_type] = (map[item.project_type] || 0) + 1
|
||||
return map
|
||||
}, {})
|
||||
@@ -544,7 +525,7 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
return options
|
||||
})
|
||||
|
||||
const selectedFilters = ref([])
|
||||
const selectedFilters = ref<string[]>([])
|
||||
const filteredProjects = computed(() => {
|
||||
const updatesFilter = selectedFilters.value.includes('updates')
|
||||
const disabledFilter = selectedFilters.value.includes('disabled')
|
||||
@@ -571,7 +552,7 @@ watch(filterOptions, () => {
|
||||
}
|
||||
})
|
||||
|
||||
function toggleArray(array, value) {
|
||||
function toggleArray<T>(array: T[], value: T) {
|
||||
if (array.includes(value)) {
|
||||
array.splice(array.indexOf(value), 1)
|
||||
} else {
|
||||
@@ -581,7 +562,7 @@ function toggleArray(array, value) {
|
||||
|
||||
const searchFilter = ref('')
|
||||
const selectAll = ref(false)
|
||||
const shareModal = ref(null)
|
||||
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
|
||||
const ascending = ref(true)
|
||||
const sortColumn = ref('Name')
|
||||
const currentPage = ref(1)
|
||||
@@ -622,7 +603,7 @@ const search = computed(() => {
|
||||
|
||||
watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1))
|
||||
|
||||
const sortProjects = (filter) => {
|
||||
const sortProjects = (filter: string) => {
|
||||
if (sortColumn.value === filter) {
|
||||
ascending.value = !ascending.value
|
||||
} else {
|
||||
@@ -640,7 +621,7 @@ const updateAll = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const paths = await update_all(props.instance.path).catch(handleError)
|
||||
const paths = (await update_all(props.instance.path).catch(handleError)) as Record<string, string>
|
||||
|
||||
for (const [oldVal, newVal] of Object.entries(paths)) {
|
||||
const index = projects.value.findIndex((x) => x.path === oldVal)
|
||||
@@ -649,7 +630,7 @@ const updateAll = async () => {
|
||||
|
||||
if (projects.value[index].updateVersion) {
|
||||
projects.value[index].version = projects.value[index].updateVersion.version_number
|
||||
projects.value[index].updateVersion = null
|
||||
projects.value[index].updateVersion = undefined
|
||||
}
|
||||
}
|
||||
for (const project of setProjects) {
|
||||
@@ -664,15 +645,15 @@ const updateAll = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateProject = async (mod) => {
|
||||
const updateProject = async (mod: ProjectListEntry) => {
|
||||
mod.updating = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
|
||||
mod.updating = false
|
||||
|
||||
mod.outdated = false
|
||||
mod.version = mod.updateVersion.version_number
|
||||
mod.updateVersion = null
|
||||
mod.version = mod.updateVersion?.version_number
|
||||
mod.updateVersion = undefined
|
||||
|
||||
trackEvent('InstanceProjectUpdate', {
|
||||
loader: props.instance.loader,
|
||||
@@ -683,15 +664,15 @@ const updateProject = async (mod) => {
|
||||
})
|
||||
}
|
||||
|
||||
const locks = {}
|
||||
const locks: Record<string, string | null> = {}
|
||||
|
||||
const toggleDisableMod = async (mod) => {
|
||||
const toggleDisableMod = async (mod: ProjectListEntry) => {
|
||||
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
|
||||
const lock = locks[mod.file_name]
|
||||
|
||||
while (lock) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout((_) => resolve(), 100)
|
||||
setTimeout((value: unknown) => resolve(value), 100)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -716,20 +697,20 @@ const toggleDisableMod = async (mod) => {
|
||||
locks[mod.file_name] = null
|
||||
}
|
||||
|
||||
const removeMod = async (mod) => {
|
||||
const removeMod = async (mod: ContentItem<ProjectListEntry>) => {
|
||||
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
||||
|
||||
trackEvent('InstanceProjectRemove', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
id: mod.id,
|
||||
name: mod.name,
|
||||
project_type: mod.project_type,
|
||||
id: mod.data.id,
|
||||
name: mod.data.name,
|
||||
project_type: mod.data.project_type,
|
||||
})
|
||||
}
|
||||
|
||||
const copyModLink = async (mod) => {
|
||||
const copyModLink = async (mod: ContentItem<ProjectListEntry>) => {
|
||||
await navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`,
|
||||
)
|
||||
@@ -744,15 +725,15 @@ const deleteSelected = async () => {
|
||||
}
|
||||
|
||||
const shareNames = async () => {
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
await shareModal.value?.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
}
|
||||
|
||||
const shareFileNames = async () => {
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.file_name).join('\n'))
|
||||
await shareModal.value?.show(functionValues.value.map((x) => x.file_name).join('\n'))
|
||||
}
|
||||
|
||||
const shareUrls = async () => {
|
||||
await shareModal.value.show(
|
||||
await shareModal.value?.show(
|
||||
functionValues.value
|
||||
.filter((x) => x.slug)
|
||||
.map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`)
|
||||
@@ -761,7 +742,7 @@ const shareUrls = async () => {
|
||||
}
|
||||
|
||||
const shareMarkdown = async () => {
|
||||
await shareModal.value.show(
|
||||
await shareModal.value?.show(
|
||||
functionValues.value
|
||||
.map((x) => {
|
||||
if (x.slug) {
|
||||
@@ -826,15 +807,17 @@ const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
await initProjects()
|
||||
})
|
||||
|
||||
const unlistenProfiles = await profile_listener(async (event) => {
|
||||
if (
|
||||
event.profile_path_id === props.instance.path &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing'
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
})
|
||||
const unlistenProfiles = await profile_listener(
|
||||
async (event: { event: string; profile_path_id: string }) => {
|
||||
if (
|
||||
event.profile_path_id === props.instance.path &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing'
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<FilterBar v-model="filters" :options="filterOptions" />
|
||||
<FilterBar v-model="filters" :options="filterOptions" show-all-options />
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<WorldItem
|
||||
v-for="world in filteredWorlds"
|
||||
@@ -225,6 +225,11 @@ const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||
await refreshAllWorlds()
|
||||
|
||||
async function refreshServer(address: string) {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
}
|
||||
}
|
||||
await refreshServerData(serverData.value[address], protocolVersion.value, address)
|
||||
}
|
||||
|
||||
@@ -263,9 +268,12 @@ async function addServer(server: ServerWorld) {
|
||||
async function editServer(server: ServerWorld) {
|
||||
const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index)
|
||||
if (index !== -1) {
|
||||
const oldServer = worlds.value[index] as ServerWorld
|
||||
worlds.value[index] = server
|
||||
sortWorlds(worlds.value)
|
||||
await refreshServer(server.address)
|
||||
if (oldServer.address !== server.address) {
|
||||
await refreshServer(server.address)
|
||||
}
|
||||
} else {
|
||||
handleError(`Error refreshing server, refreshing all worlds`)
|
||||
await refreshAllWorlds()
|
||||
@@ -349,26 +357,34 @@ const supportsQuickPlay = computed(() =>
|
||||
const filterOptions = computed(() => {
|
||||
const options: FilterBarOption[] = []
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'singleplayer')) {
|
||||
const hasServer = worlds.value.some((x) => x.type === 'server')
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'singleplayer') && hasServer) {
|
||||
options.push({
|
||||
id: 'singleplayer',
|
||||
message: messages.singleplayer,
|
||||
})
|
||||
}
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'server')) {
|
||||
options.push({
|
||||
id: 'server',
|
||||
message: messages.server,
|
||||
})
|
||||
}
|
||||
|
||||
// add available filter if there's any offline ("unavailable") servers
|
||||
if (hasServer) {
|
||||
// add available filter if there's any offline ("unavailable") servers AND there's any singleplayer worlds or available servers
|
||||
if (
|
||||
worlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'server' &&
|
||||
!serverData.value[x.address]?.status &&
|
||||
!serverData.value[x.address]?.refreshing,
|
||||
) &&
|
||||
worlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'singleplayer' ||
|
||||
(x.type === 'server' &&
|
||||
serverData.value[x.address]?.status &&
|
||||
!serverData.value[x.address]?.refreshing),
|
||||
)
|
||||
) {
|
||||
options.push({
|
||||
|
||||
@@ -155,7 +155,7 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
@@ -170,6 +170,7 @@ import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -247,6 +248,9 @@ async function install(version) {
|
||||
installedVersion.value = version
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ export const useInstall = defineStore('installStore', {
|
||||
setInstallConfirmModal(ref) {
|
||||
this.installConfirmModal = ref
|
||||
},
|
||||
showInstallConfirmModal(project, version_id, onInstall) {
|
||||
this.installConfirmModal.show(project, version_id, onInstall)
|
||||
showInstallConfirmModal(project, version_id, onInstall, createInstanceCallback) {
|
||||
this.installConfirmModal.show(project, version_id, onInstall, createInstanceCallback)
|
||||
},
|
||||
setIncompatibilityWarningModal(ref) {
|
||||
this.incompatibilityWarningModal = ref
|
||||
@@ -41,7 +41,14 @@ export const useInstall = defineStore('installStore', {
|
||||
},
|
||||
})
|
||||
|
||||
export const install = async (projectId, versionId, instancePath, source, callback = () => {}) => {
|
||||
export const install = async (
|
||||
projectId,
|
||||
versionId,
|
||||
instancePath,
|
||||
source,
|
||||
callback = () => {},
|
||||
createInstanceCallback = () => {},
|
||||
) => {
|
||||
const project = await get_project(projectId, 'must_revalidate').catch(handleError)
|
||||
|
||||
if (project.project_type === 'modpack') {
|
||||
@@ -49,7 +56,13 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
||||
const packs = await list().catch(handleError)
|
||||
|
||||
if (packs.length === 0 || !packs.find((pack) => pack.linked_data?.project_id === project.id)) {
|
||||
await packInstall(project.id, version, project.title, project.icon_url).catch(handleError)
|
||||
await packInstall(
|
||||
project.id,
|
||||
version,
|
||||
project.title,
|
||||
project.icon_url,
|
||||
createInstanceCallback,
|
||||
).catch(handleError)
|
||||
|
||||
trackEvent('PackInstall', {
|
||||
id: project.id,
|
||||
@@ -61,7 +74,7 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
||||
callback(version)
|
||||
} else {
|
||||
const install = useInstall()
|
||||
install.showInstallConfirmModal(project, version, callback)
|
||||
install.showInstallConfirmModal(project, version, callback, createInstanceCallback)
|
||||
}
|
||||
} else {
|
||||
if (instancePath) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTheming } from './theme'
|
||||
import { useTheming } from './theme.ts'
|
||||
import { useBreadcrumbs } from './breadcrumbs'
|
||||
import { useLoading } from './loading'
|
||||
import { useNotifications, handleError } from './notifications'
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useTheming = defineStore('themeStore', {
|
||||
state: () => ({
|
||||
themeOptions: ['dark', 'light', 'oled', 'system'],
|
||||
advancedRendering: true,
|
||||
selectedTheme: 'dark',
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: {},
|
||||
}),
|
||||
actions: {
|
||||
setThemeState(newTheme) {
|
||||
if (this.themeOptions.includes(newTheme)) this.selectedTheme = newTheme
|
||||
else console.warn('Selected theme is not present. Check themeOptions.')
|
||||
|
||||
this.setThemeClass()
|
||||
},
|
||||
setThemeClass() {
|
||||
for (const theme of this.themeOptions) {
|
||||
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
||||
}
|
||||
|
||||
let theme = this.selectedTheme
|
||||
if (this.selectedTheme === 'system') {
|
||||
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (darkThemeMq.matches) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(`${theme}-mode`)
|
||||
},
|
||||
},
|
||||
})
|
||||
70
apps/app-frontend/src/store/theme.ts
Normal file
70
apps/app-frontend/src/store/theme.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = {
|
||||
project_background: false,
|
||||
page_path: false,
|
||||
worlds_tab: false,
|
||||
worlds_in_home: true,
|
||||
}
|
||||
|
||||
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
|
||||
|
||||
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
|
||||
export type FeatureFlags = Record<FeatureFlag, boolean>
|
||||
export type ColorTheme = (typeof THEME_OPTIONS)[number]
|
||||
|
||||
export type ThemeStore = {
|
||||
selectedTheme: ColorTheme
|
||||
advancedRendering: boolean
|
||||
toggleSidebar: boolean
|
||||
|
||||
devMode: boolean
|
||||
featureFlags: FeatureFlags
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME_STORE: ThemeStore = {
|
||||
selectedTheme: 'dark',
|
||||
advancedRendering: true,
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: DEFAULT_FEATURE_FLAGS,
|
||||
}
|
||||
|
||||
export const useTheming = defineStore('themeStore', {
|
||||
state: () => DEFAULT_THEME_STORE,
|
||||
actions: {
|
||||
setThemeState(newTheme: ColorTheme) {
|
||||
if (THEME_OPTIONS.includes(newTheme)) {
|
||||
this.selectedTheme = newTheme
|
||||
} else {
|
||||
console.warn('Selected theme is not present. Check themeOptions.')
|
||||
}
|
||||
|
||||
this.setThemeClass()
|
||||
},
|
||||
setThemeClass() {
|
||||
for (const theme of THEME_OPTIONS) {
|
||||
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
||||
}
|
||||
|
||||
let theme = this.selectedTheme
|
||||
if (this.selectedTheme === 'system') {
|
||||
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (darkThemeMq.matches) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(`${theme}-mode`)
|
||||
},
|
||||
getFeatureFlag(key: FeatureFlag) {
|
||||
return this.featureFlags[key] ?? DEFAULT_FEATURE_FLAGS[key]
|
||||
},
|
||||
getThemeOptions() {
|
||||
return THEME_OPTIONS
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -9,5 +9,6 @@ edition = "2021"
|
||||
theseus = { path = "../../packages/app-lib", features = ["cli"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
webbrowser = "0.8.13"
|
||||
enumset = "1.1"
|
||||
|
||||
tracing = "0.1.37"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use enumset::EnumSet;
|
||||
use theseus::prelude::*;
|
||||
use theseus::worlds::get_recent_worlds;
|
||||
|
||||
@@ -40,7 +41,7 @@ async fn main() -> theseus::Result<()> {
|
||||
// Initialize state
|
||||
State::init().await?;
|
||||
|
||||
let worlds = get_recent_worlds(4).await?;
|
||||
let worlds = get_recent_worlds(4, EnumSet::all()).await?;
|
||||
for world in worlds {
|
||||
println!(
|
||||
"World: {:?}/{:?} played at {:?}: {:#?}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
@@ -42,6 +42,7 @@ tracing-error = "0.2.0"
|
||||
|
||||
dashmap = "6.0.1"
|
||||
paste = "1.0.15"
|
||||
enumset = { version = "1.1", features = ["serde"] }
|
||||
|
||||
opener = { version = "0.7.2", features = ["reveal", "dbus-vendored"] }
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ fn main() {
|
||||
"get_recent_worlds",
|
||||
"get_profile_worlds",
|
||||
"get_singleplayer_world",
|
||||
"set_world_display_status",
|
||||
"rename_world",
|
||||
"reset_world_icon",
|
||||
"backup_world",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::api::Result;
|
||||
use either::Either;
|
||||
use enumset::EnumSet;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use theseus::prelude::ProcessMetadata;
|
||||
use theseus::profile::{get_full_path, QuickPlayType};
|
||||
use theseus::worlds::{
|
||||
ServerPackStatus, ServerStatus, World, WorldWithProfile,
|
||||
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
||||
WorldWithProfile,
|
||||
};
|
||||
use theseus::{profile, worlds};
|
||||
|
||||
@@ -14,6 +16,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
get_recent_worlds,
|
||||
get_profile_worlds,
|
||||
get_singleplayer_world,
|
||||
set_world_display_status,
|
||||
rename_world,
|
||||
reset_world_icon,
|
||||
backup_world,
|
||||
@@ -33,8 +36,13 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
pub async fn get_recent_worlds<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
limit: usize,
|
||||
display_statuses: Option<EnumSet<DisplayStatus>>,
|
||||
) -> Result<Vec<WorldWithProfile>> {
|
||||
let mut result = worlds::get_recent_worlds(limit).await?;
|
||||
let mut result = worlds::get_recent_worlds(
|
||||
limit,
|
||||
display_statuses.unwrap_or(EnumSet::all()),
|
||||
)
|
||||
.await?;
|
||||
for world in result.iter_mut() {
|
||||
adapt_world_icon(&app_handle, &mut world.world);
|
||||
}
|
||||
@@ -59,8 +67,7 @@ pub async fn get_singleplayer_world<R: Runtime>(
|
||||
instance: &str,
|
||||
world: &str,
|
||||
) -> Result<World> {
|
||||
let instance = get_full_path(instance).await?;
|
||||
let mut world = worlds::get_singleplayer_world(&instance, world).await?;
|
||||
let mut world = worlds::get_singleplayer_world(instance, world).await?;
|
||||
adapt_world_icon(&app_handle, &mut world);
|
||||
Ok(world)
|
||||
}
|
||||
@@ -90,6 +97,22 @@ fn adapt_world_icon<R: Runtime>(app_handle: &AppHandle<R>, world: &mut World) {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_world_display_status(
|
||||
instance: &str,
|
||||
world_type: WorldType,
|
||||
world_id: &str,
|
||||
display_status: DisplayStatus,
|
||||
) -> Result<()> {
|
||||
Ok(worlds::set_world_display_status(
|
||||
instance,
|
||||
world_type,
|
||||
world_id,
|
||||
display_status,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rename_world(
|
||||
instance: &str,
|
||||
|
||||
@@ -117,8 +117,7 @@ fn show_window(app: tauri::AppHandle) {
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text(&format!(
|
||||
"Cannot display application window due to an error:\n{}",
|
||||
e
|
||||
"Cannot display application window due to an error:\n{e}"
|
||||
))
|
||||
.show_alert()
|
||||
.unwrap();
|
||||
@@ -138,8 +137,7 @@ fn is_dev() -> bool {
|
||||
async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
|
||||
window.set_decorations(b).map_err(|e| {
|
||||
theseus::Error::from(theseus::ErrorKind::OtherError(format!(
|
||||
"Failed to toggle decorations: {}",
|
||||
e
|
||||
"Failed to toggle decorations: {e}"
|
||||
)))
|
||||
})?;
|
||||
Ok(())
|
||||
@@ -320,7 +318,7 @@ fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
|
||||
if format!("{:?}", e).contains(
|
||||
if format!("{e:?}").contains(
|
||||
"Runtime(CreateWebview(WebView2Error(WindowsError",
|
||||
) {
|
||||
MessageDialog::new()
|
||||
@@ -338,8 +336,7 @@ fn main() {
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text(&format!(
|
||||
"Cannot initialize application due to an error:\n{:?}",
|
||||
e
|
||||
"Cannot initialize application due to an error:\n{e:?}"
|
||||
))
|
||||
.show_alert()
|
||||
.unwrap();
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
@@ -83,7 +83,7 @@
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/inter/"],
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"script-src": "https://*.posthog.com 'self'",
|
||||
|
||||
@@ -57,7 +57,7 @@ pub async fn fetch_forge(
|
||||
|
||||
ForgeVersion {
|
||||
format_version,
|
||||
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{loader_version}/forge-{loader_version}-installer.jar"),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: game_version.clone(),
|
||||
@@ -137,7 +137,7 @@ pub async fn fetch_neo(
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{loader_version}/forge-{loader_version}-installer.jar"),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: "1.20.1".to_string(), // All NeoForge Forge versions are for 1.20.1
|
||||
@@ -163,7 +163,7 @@ pub async fn fetch_neo(
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{0}/neoforge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{loader_version}/neoforge-{loader_version}-installer.jar"),
|
||||
loader_version: loader_version.clone(),
|
||||
raw: loader_version,
|
||||
game_version,
|
||||
@@ -502,7 +502,7 @@ async fn fetch(
|
||||
)?;
|
||||
|
||||
artifact.url =
|
||||
format_url(&format!("maven/{}", artifact_path));
|
||||
format_url(&format!("maven/{artifact_path}"));
|
||||
|
||||
return Ok(lib);
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@
|
||||
<UpdatedIcon />
|
||||
</Button>
|
||||
<DropdownSelect
|
||||
class="range-dropdown"
|
||||
v-model="selectedRange"
|
||||
class="range-dropdown"
|
||||
:options="ranges"
|
||||
name="Time range"
|
||||
:display-name="
|
||||
@@ -197,11 +197,11 @@
|
||||
>
|
||||
<div class="country-flag-container">
|
||||
<template v-if="name.toLowerCase() === 'xx' || !name">
|
||||
<img
|
||||
src="https://cdn.modrinth.com/placeholder-banner.svg"
|
||||
alt="Placeholder flag"
|
||||
class="country-flag"
|
||||
/>
|
||||
<div
|
||||
class="country-flag flex select-none items-center justify-center bg-bg-raised font-extrabold text-secondary"
|
||||
>
|
||||
?
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<img
|
||||
@@ -213,7 +213,7 @@
|
||||
</div>
|
||||
<div class="country-text">
|
||||
<strong class="country-name"
|
||||
><template v-if="name.toLowerCase() === 'xx' || !name">Hidden</template>
|
||||
><template v-if="name.toLowerCase() === 'xx' || !name">Other</template>
|
||||
<template v-else>{{ countryCodeToName(name) }}</template>
|
||||
</strong>
|
||||
<span class="data-point">{{ formatNumber(count) }}</span>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="text-sm font-bold text-contrast">Minecraft version</div>
|
||||
<div class="text-lg font-bold text-contrast">Minecraft version</div>
|
||||
<UiServersTeleportDropdownMenu
|
||||
v-model="selectedMCVersion"
|
||||
name="mcVersion"
|
||||
@@ -61,6 +61,20 @@
|
||||
class="w-full max-w-[100%]"
|
||||
placeholder="Select Minecraft version..."
|
||||
/>
|
||||
<div class="mt-2 flex items-center justify-between gap-2">
|
||||
<label for="toggle-snapshots" class="font-semibold"> Show snapshot versions </label>
|
||||
<div
|
||||
v-tooltip="
|
||||
isSnapshotSelected ? 'A snapshot version is currently selected.' : undefined
|
||||
"
|
||||
>
|
||||
<Toggle
|
||||
id="toggle-snapshots"
|
||||
v-model="showSnapshots"
|
||||
:disabled="isSnapshotSelected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -74,7 +88,7 @@
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-bold text-contrast">{{ selectedLoader }} version</div>
|
||||
<div class="text-lg font-bold text-contrast">{{ selectedLoader }} version</div>
|
||||
|
||||
<template v-if="!selectedMCVersion">
|
||||
<div
|
||||
@@ -177,8 +191,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { RightArrowIcon, XIcon, ServerIcon, DropdownIcon } from "@modrinth/assets";
|
||||
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
||||
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
||||
import { $fetch } from "ofetch";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { Loaders } from "~/types/servers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
@@ -214,6 +229,7 @@ const hardReset = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const loadingServerCheck = ref(false);
|
||||
const serverCheckError = ref("");
|
||||
const showSnapshots = ref(false);
|
||||
|
||||
const selectedLoader = ref<Loaders>("Vanilla");
|
||||
const selectedMCVersion = ref("");
|
||||
@@ -226,6 +242,22 @@ const cachedVersions = ref<VersionCache>({});
|
||||
|
||||
const versionStrings = ["forge", "fabric", "quilt", "neo"] as const;
|
||||
|
||||
const isSnapshotSelected = computed(() => {
|
||||
if (selectedMCVersion.value) {
|
||||
const selected = tags.value.gameVersions.find((x) => x.version === selectedMCVersion.value);
|
||||
if (selected?.version_type !== "release") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const getLoaderVersions = async (loader: string) => {
|
||||
return await $fetch(
|
||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/manifest.json`,
|
||||
);
|
||||
};
|
||||
|
||||
const fetchLoaderVersions = async () => {
|
||||
const versions = await Promise.all(
|
||||
versionStrings.map(async (loader) => {
|
||||
@@ -234,7 +266,7 @@ const fetchLoaderVersions = async () => {
|
||||
throw new Error("Failed to fetch loader versions");
|
||||
}
|
||||
try {
|
||||
const res = await $fetch(`/loader-versions?loader=${loader}`);
|
||||
const res = await getLoaderVersions(loader);
|
||||
return { [loader]: (res as any).gameVersions };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (_) {
|
||||
@@ -277,11 +309,11 @@ const fetchPurpurVersions = async (mcVersion: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedLoaderVersions = computed(() => {
|
||||
const selectedLoaderVersions = computed<string[]>(() => {
|
||||
const loader = selectedLoader.value.toLowerCase();
|
||||
|
||||
if (loader === "paper") {
|
||||
return paperVersions.value[selectedMCVersion.value] || [];
|
||||
return paperVersions.value[selectedMCVersion.value].map((x) => `${x}`) || [];
|
||||
}
|
||||
|
||||
if (loader === "purpur") {
|
||||
@@ -325,13 +357,22 @@ watch(selectedLoader, async () => {
|
||||
watch(
|
||||
selectedLoaderVersions,
|
||||
(newVersions) => {
|
||||
if (newVersions.length > 0 && !selectedLoaderVersion.value) {
|
||||
if (
|
||||
newVersions.length > 0 &&
|
||||
(!selectedLoaderVersion.value || !newVersions.includes(selectedLoaderVersion.value))
|
||||
) {
|
||||
selectedLoaderVersion.value = String(newVersions[0]);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const getLoaderVersion = async (loader: string, version: string) => {
|
||||
return await $fetch(
|
||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/versions/${version}.json`,
|
||||
);
|
||||
};
|
||||
|
||||
const checkVersionAvailability = async (version: string) => {
|
||||
if (!version || version.trim().length < 3) return;
|
||||
|
||||
@@ -339,9 +380,7 @@ const checkVersionAvailability = async (version: string) => {
|
||||
loadingServerCheck.value = true;
|
||||
|
||||
try {
|
||||
const mcRes =
|
||||
cachedVersions.value[version] ||
|
||||
(await $fetch(`/loader-versions?loader=minecraft&version=${version}`));
|
||||
const mcRes = cachedVersions.value[version] || (await getLoaderVersion("minecraft", version));
|
||||
|
||||
cachedVersions.value[version] = mcRes;
|
||||
|
||||
@@ -377,13 +416,15 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const tags = useTags();
|
||||
const mcVersions = tags.value.gameVersions
|
||||
.filter((x) => x.version_type === "release")
|
||||
.map((x) => x.version)
|
||||
.filter((x) => {
|
||||
const segment = parseInt(x.split(".")[1], 10);
|
||||
return !isNaN(segment) && segment > 2;
|
||||
});
|
||||
const mcVersions = computed(() =>
|
||||
tags.value.gameVersions
|
||||
.filter((x) =>
|
||||
showSnapshots.value
|
||||
? x.version_type === "snapshot" || x.version_type === "release"
|
||||
: x.version_type === "release",
|
||||
)
|
||||
.map((x) => x.version),
|
||||
);
|
||||
|
||||
const isDangerous = computed(() => hardReset.value);
|
||||
const canInstall = computed(() => {
|
||||
@@ -448,6 +489,9 @@ const handleReinstall = async () => {
|
||||
|
||||
const onShow = () => {
|
||||
selectedMCVersion.value = props.server.general?.mc_version || "";
|
||||
if (isSnapshotSelected.value) {
|
||||
showSnapshots.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onHide = () => {
|
||||
|
||||
@@ -638,6 +638,7 @@
|
||||
shown: !isMember,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
]"
|
||||
aria-label="More options"
|
||||
:dropdown-id="`${baseId}-more-options`"
|
||||
@@ -659,6 +660,10 @@
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy ID
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy permanent link
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -888,6 +893,7 @@ import { reportProject } from "~/utils/report-helpers.ts";
|
||||
|
||||
const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const auth = await useAuth();
|
||||
const user = await useUser();
|
||||
@@ -1458,6 +1464,10 @@ async function copyId() {
|
||||
await navigator.clipboard.writeText(project.value.id);
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
await navigator.clipboard.writeText(`${config.public.siteUrl}/project/${project.value.id}`);
|
||||
}
|
||||
|
||||
const collapsedChecklist = ref(false);
|
||||
|
||||
const showModerationChecklist = ref(false);
|
||||
|
||||
@@ -622,7 +622,7 @@
|
||||
<CopyCode :text="version.id" />
|
||||
</div>
|
||||
<div v-if="!isEditing && flags.developerMode">
|
||||
<h4>Modrinth Maven</h4>
|
||||
<h4>Maven coordinates</h4>
|
||||
<div class="maven-section">
|
||||
<CopyCode :text="`maven.modrinth:${project.id}:${version.id}`" />
|
||||
</div>
|
||||
@@ -1555,6 +1555,10 @@ export default defineNuxtComponent({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
button {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member {
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
</template>
|
||||
<template #copy-maven>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy Modrinth Maven
|
||||
Copy Maven coordinates
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -52,10 +52,7 @@
|
||||
>
|
||||
<div class="project-title">
|
||||
<div class="mobile-row">
|
||||
<nuxt-link
|
||||
:to="`/${project.inferred_project_type}/${project.slug}`"
|
||||
class="iconified-stacked-link"
|
||||
>
|
||||
<nuxt-link :to="`/project/${project.id}`" class="iconified-stacked-link">
|
||||
<Avatar :src="project.icon_url" size="xs" no-shadow raised />
|
||||
<span class="stacked">
|
||||
<span class="title">{{ project.name }}</span>
|
||||
@@ -67,7 +64,7 @@
|
||||
by
|
||||
<nuxt-link
|
||||
v-if="project.owner"
|
||||
:to="`/user/${project.owner.user.username}`"
|
||||
:to="`/user/${project.owner.user.id}`"
|
||||
class="iconified-link"
|
||||
>
|
||||
<Avatar :src="project.owner.user.avatar_url" circle size="xxs" raised />
|
||||
@@ -75,7 +72,7 @@
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-else-if="project.org"
|
||||
:to="`/organization/${project.org.slug}`"
|
||||
:to="`/organization/${project.org.id}`"
|
||||
class="iconified-link"
|
||||
>
|
||||
<Avatar :src="project.org.icon_url" circle size="xxs" raised />
|
||||
@@ -88,10 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<nuxt-link
|
||||
:to="`/${project.inferred_project_type}/${project.slug}`"
|
||||
class="iconified-button raised-button"
|
||||
>
|
||||
<nuxt-link :to="`/project/${project.id}`" class="iconified-button raised-button">
|
||||
<EyeIcon />
|
||||
View project
|
||||
</nuxt-link>
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
},
|
||||
{ divider: true, shown: auth.user && currentMember },
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
@@ -135,6 +136,10 @@
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyPermalinkButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -287,6 +292,7 @@ const cosmetics = useCosmetics();
|
||||
const route = useNativeRoute();
|
||||
const tags = useTags();
|
||||
const flags = useFeatureFlags();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
let orgId = useRouteId();
|
||||
|
||||
@@ -502,6 +508,12 @@ const navLinks = computed(() => [
|
||||
async function copyId() {
|
||||
await navigator.clipboard.writeText(organization.value.id);
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
await navigator.clipboard.writeText(
|
||||
`${config.public.siteUrl}/organization/${organization.value.id}`,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="experimental-styles-within flex flex-col gap-2">
|
||||
<RadialHeader class="top-box mb-2 text-center" color="orange">
|
||||
<RadialHeader class="top-box mb-2 flex flex-col items-center justify-center" color="orange">
|
||||
<ScaleIcon class="h-12 w-12 text-brand-orange" />
|
||||
<h1 class="m-3 gap-2 text-3xl font-extrabold">
|
||||
{{
|
||||
|
||||
@@ -64,6 +64,21 @@
|
||||
</template>
|
||||
</span>
|
||||
<template v-if="midasCharge">
|
||||
<span
|
||||
v-if="
|
||||
midasCharge.status === 'open' && midasCharge.subscription_interval === 'monthly'
|
||||
"
|
||||
class="text-sm text-purple"
|
||||
>
|
||||
Save
|
||||
{{
|
||||
formatPrice(
|
||||
vintl.locale,
|
||||
midasCharge.amount * 12 - oppositePrice,
|
||||
midasCharge.currency_code,
|
||||
)
|
||||
}}/year by switching to yearly billing!
|
||||
</span>
|
||||
<span class="text-sm text-secondary">
|
||||
Since {{ $dayjs(midasSubscription.created).format("MMMM D, YYYY") }}
|
||||
</span>
|
||||
@@ -118,19 +133,46 @@
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled v-else-if="midasCharge && midasCharge.status !== 'cancelled'">
|
||||
<button
|
||||
class="ml-auto"
|
||||
@click="
|
||||
() => {
|
||||
cancelSubscriptionId = midasSubscription.id;
|
||||
$refs.modalCancel.show();
|
||||
}
|
||||
"
|
||||
<div
|
||||
v-else-if="midasCharge && midasCharge.status !== 'cancelled'"
|
||||
class="ml-auto flex gap-2"
|
||||
>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
:disabled="changingInterval"
|
||||
@click="
|
||||
() => {
|
||||
cancelSubscriptionId = midasSubscription.id;
|
||||
$refs.modalCancel.show();
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon /> Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
:color="midasCharge.subscription_interval === 'yearly' ? 'standard' : 'purple'"
|
||||
color-fill="text"
|
||||
>
|
||||
<XIcon /> Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<button
|
||||
v-tooltip="
|
||||
midasCharge.subscription_interval === 'yearly'
|
||||
? `Monthly billing will cost you an additional ${formatPrice(
|
||||
vintl.locale,
|
||||
oppositePrice * 12 - midasCharge.amount,
|
||||
midasCharge.currency_code,
|
||||
)} per year`
|
||||
: undefined
|
||||
"
|
||||
:disabled="changingInterval"
|
||||
@click="switchMidasInterval(oppositeInterval)"
|
||||
>
|
||||
<SpinnerIcon v-if="changingInterval" class="animate-spin" />
|
||||
<TransferIcon v-else /> {{ changingInterval ? "Switching" : "Switch" }} to
|
||||
{{ oppositeInterval }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
v-else-if="midasCharge && midasCharge.status === 'cancelled'"
|
||||
color="purple"
|
||||
@@ -178,8 +220,12 @@
|
||||
/>
|
||||
<div v-else class="w-fit">
|
||||
<p>
|
||||
A linked server couldn't be found with this subscription. It may have been deleted
|
||||
or suspended. Please contact Modrinth support with the following information:
|
||||
A linked server couldn't be found for this subscription. There are a few possible
|
||||
explanations for this. If you just purchased your server, this is normal. It could
|
||||
take up to an hour for your server to be provisioned. Otherwise, if you purchased
|
||||
this server a while ago, it has likely since been suspended. If this is not what
|
||||
you were expecting, please contact Modrinth support with the following
|
||||
information:
|
||||
</p>
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<CopyCode
|
||||
@@ -547,6 +593,8 @@ import {
|
||||
} from "@modrinth/ui";
|
||||
import {
|
||||
PlusIcon,
|
||||
TransferIcon,
|
||||
SpinnerIcon,
|
||||
ArrowBigUpDashIcon,
|
||||
XIcon,
|
||||
CardIcon,
|
||||
@@ -750,6 +798,13 @@ const midasCharge = computed(() =>
|
||||
: null,
|
||||
);
|
||||
|
||||
const oppositePrice = computed(() =>
|
||||
midasSubscription.value
|
||||
? midasProduct.value?.prices?.find((price) => price.id === midasSubscription.value.price_id)
|
||||
?.prices?.intervals?.[oppositeInterval.value]
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const pyroSubscriptions = computed(() => {
|
||||
const pyroSubs = subscriptions.value?.filter((s) => s?.metadata?.type === "pyro") || [];
|
||||
const servers = serversData.value?.servers || [];
|
||||
@@ -847,6 +902,31 @@ async function submit() {
|
||||
|
||||
const removePaymentMethodIndex = ref();
|
||||
|
||||
const changingInterval = ref(false);
|
||||
|
||||
const oppositeInterval = computed(() =>
|
||||
midasCharge.value?.subscription_interval === "yearly" ? "monthly" : "yearly",
|
||||
);
|
||||
|
||||
async function switchMidasInterval(interval) {
|
||||
changingInterval.value = true;
|
||||
startLoading();
|
||||
try {
|
||||
await useBaseFetch(`billing/subscription/${midasSubscription.value.id}`, {
|
||||
internal: true,
|
||||
method: "PATCH",
|
||||
body: {
|
||||
interval,
|
||||
},
|
||||
});
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
console.error("Error switching Modrinth+ payment interval:", error);
|
||||
}
|
||||
stopLoading();
|
||||
changingInterval.value = false;
|
||||
}
|
||||
|
||||
async function editPaymentMethod(index, primary) {
|
||||
startLoading();
|
||||
try {
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
shown: auth.user?.id !== user.id,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||
{
|
||||
id: 'open-billing',
|
||||
action: () => navigateTo(`/admin/billing/${user.id}`),
|
||||
@@ -151,6 +152,10 @@
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyPermalinkButton) }}
|
||||
</template>
|
||||
<template #open-billing>
|
||||
<CurrencyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.billingButton) }}
|
||||
@@ -381,6 +386,7 @@ const auth = await useAuth();
|
||||
const cosmetics = useCosmetics();
|
||||
const tags = useTags();
|
||||
const flags = useFeatureFlags();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const vintl = useVIntl();
|
||||
const { formatMessage } = vintl;
|
||||
@@ -616,6 +622,10 @@ async function copyId() {
|
||||
await navigator.clipboard.writeText(user.value.id);
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
await navigator.clipboard.writeText(`${config.public.siteUrl}/user/${user.value.id}`);
|
||||
}
|
||||
|
||||
const navLinks = computed(() => [
|
||||
{
|
||||
label: formatMessage(commonMessages.allProjectType),
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
const getLoaderVersions = async (loader: string) => {
|
||||
const loaderVersions = await fetch(
|
||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/manifest.json`,
|
||||
);
|
||||
return loaderVersions.json();
|
||||
};
|
||||
|
||||
const getLoaderVersion = async (loader: string, version: string) => {
|
||||
const loaderVersion = await fetch(
|
||||
`https://launcher-meta.modrinth.com/${loader?.toLowerCase()}/v0/versions/${version}.json`,
|
||||
);
|
||||
return loaderVersion.json();
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const params = new URLSearchParams(e._path?.split("?")[1] ?? "");
|
||||
if (!params.has("loader"))
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Missing loader",
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
const loader = params.get("loader");
|
||||
const version = params.get("version");
|
||||
if (version) {
|
||||
const loaderVersion = await getLoaderVersion(loader!, version);
|
||||
return new Response(JSON.stringify(loaderVersion), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const loaderVersions = await getLoaderVersions(loader!);
|
||||
return new Response(JSON.stringify(loaderVersions), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
@@ -262,8 +262,9 @@ export const processAnalyticsByCountry = (category, projects, sortFn) => {
|
||||
|
||||
loadedProjectData.forEach((data) => {
|
||||
Object.entries(data).forEach(([country, value]) => {
|
||||
const current = countrySums.get(country) || 0;
|
||||
countrySums.set(country, current + value);
|
||||
const countryCode = country || "XX";
|
||||
const current = countrySums.get(countryCode) || 0;
|
||||
countrySums.set(countryCode, current + value);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ impl actix_web::ResponseError for OAuthError {
|
||||
);
|
||||
|
||||
if let Some(state) = self.state.as_ref() {
|
||||
redirect_uri = format!("{}&state={}", redirect_uri, state);
|
||||
redirect_uri = format!("{redirect_uri}&state={state}");
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
|
||||
@@ -414,7 +414,7 @@ fn generate_access_token() -> String {
|
||||
.take(60)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
format!("mro_{}", random)
|
||||
format!("mro_{random}")
|
||||
}
|
||||
|
||||
async fn init_oauth_code_flow(
|
||||
|
||||
@@ -32,7 +32,7 @@ impl Display for ErrorPage {
|
||||
let html = include_str!("error.html")
|
||||
.replace("{{ code }}", &self.code.to_string())
|
||||
.replace("{{ message }}", &self.message);
|
||||
write!(f, "{}", html)?;
|
||||
write!(f, "{html}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -103,8 +103,7 @@ impl MinecraftGameVersion {
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Game version requires field value to be an enum: {:?}",
|
||||
version_field
|
||||
"Game version requires field value to be an enum: {version_field:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1080,8 +1080,7 @@ impl VersionFieldValue {
|
||||
let field_name = field_type.to_str();
|
||||
let did_not_exist_error = |field_name: &str, desired_field: &str| {
|
||||
DatabaseError::SchemaError(format!(
|
||||
"Field name {} for field {} in does not exist",
|
||||
desired_field, field_name
|
||||
"Field name {desired_field} for field {field_name} in does not exist"
|
||||
))
|
||||
};
|
||||
|
||||
@@ -1103,8 +1102,7 @@ impl VersionFieldValue {
|
||||
.collect::<Vec<_>>();
|
||||
if field_id.len() > 1 {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Multiple field ids for field {}",
|
||||
field_name
|
||||
"Multiple field ids for field {field_name}"
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -912,7 +912,7 @@ impl Version {
|
||||
file.hashes.iter().map(|(algo, hash)| {
|
||||
(
|
||||
VERSION_FILES_NAMESPACE,
|
||||
Some(format!("{}_{}", algo, hash)),
|
||||
Some(format!("{algo}_{hash}")),
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -80,10 +80,9 @@ impl From<DBNotification> for Notification {
|
||||
} => (
|
||||
"A project you follow has been updated!".to_string(),
|
||||
format!(
|
||||
"The project {} has released a new version: {}",
|
||||
project_id, version_id
|
||||
"The project {project_id} has released a new version: {version_id}"
|
||||
),
|
||||
format!("/project/{}/version/{}", project_id, version_id),
|
||||
format!("/project/{project_id}/version/{version_id}"),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::TeamInvite {
|
||||
@@ -93,8 +92,8 @@ impl From<DBNotification> for Notification {
|
||||
..
|
||||
} => (
|
||||
"You have been invited to join a team!".to_string(),
|
||||
format!("An invite has been sent for you to be {} of a team", role),
|
||||
format!("/project/{}", project_id),
|
||||
format!("An invite has been sent for you to be {role} of a team"),
|
||||
format!("/project/{project_id}"),
|
||||
vec![
|
||||
NotificationAction {
|
||||
name: "Accept".to_string(),
|
||||
@@ -117,10 +116,9 @@ impl From<DBNotification> for Notification {
|
||||
} => (
|
||||
"You have been invited to join an organization!".to_string(),
|
||||
format!(
|
||||
"An invite has been sent for you to be {} of an organization",
|
||||
role
|
||||
"An invite has been sent for you to be {role} of an organization"
|
||||
),
|
||||
format!("/organization/{}", organization_id),
|
||||
format!("/organization/{organization_id}"),
|
||||
vec![
|
||||
NotificationAction {
|
||||
name: "Accept".to_string(),
|
||||
@@ -149,7 +147,7 @@ impl From<DBNotification> for Notification {
|
||||
old_status.as_friendly_str(),
|
||||
new_status.as_friendly_str()
|
||||
),
|
||||
format!("/project/{}", project_id),
|
||||
format!("/project/{project_id}"),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ModeratorMessage {
|
||||
@@ -160,9 +158,9 @@ impl From<DBNotification> for Notification {
|
||||
"A moderator has sent you a message!".to_string(),
|
||||
"Click on the link to read more.".to_string(),
|
||||
if let Some(project_id) = project_id {
|
||||
format!("/project/{}", project_id)
|
||||
format!("/project/{project_id}")
|
||||
} else if let Some(report_id) = report_id {
|
||||
format!("/project/{}", report_id)
|
||||
format!("/project/{report_id}")
|
||||
} else {
|
||||
"#".to_string()
|
||||
},
|
||||
|
||||
@@ -168,19 +168,19 @@ impl ModerationMessage {
|
||||
|
||||
for project in &projects {
|
||||
let additional_text = if project.contains("ftb-quests") {
|
||||
Some("Heracles")
|
||||
Some(("Odyssey Quests", "lo90fZoB"))
|
||||
} else if project.contains("ftb-ranks") || project.contains("ftb-essentials") {
|
||||
Some("Prometheus")
|
||||
Some(("Odyssey Roles", "iYcNKH7W"))
|
||||
} else if project.contains("ftb-teams") {
|
||||
Some("Argonauts")
|
||||
Some(("Odyssey Guilds", "bb2EpKpx"))
|
||||
} else if project.contains("ftb-chunks") {
|
||||
Some("Cadmus")
|
||||
Some(("Odyssey Claims", "fEWKxVzh"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
val.push_str(&if let Some(additional_text) = additional_text {
|
||||
format!("- {project}(consider using [{additional_text}](https://modrinth.com/mod/{}) instead)\n", additional_text.to_lowercase())
|
||||
format!("- {project} (consider using [{}](https://modrinth.com/project/{}) instead)\n", additional_text.0, additional_text.1)
|
||||
} else {
|
||||
format!("- {project}\n")
|
||||
})
|
||||
|
||||
@@ -222,8 +222,7 @@ pub async fn delphi_result_ingest(
|
||||
for (issue, trace) in &body.issues {
|
||||
for (path, code) in trace {
|
||||
header.push_str(&format!(
|
||||
"\n issue {issue} found at file {}: \n ```\n{}\n```",
|
||||
path, code
|
||||
"\n issue {issue} found at file {path}: \n ```\n{code}\n```"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -242,10 +241,8 @@ pub async fn delphi_result_ingest(
|
||||
|
||||
for (issue, trace) in &body.issues {
|
||||
for path in trace.keys() {
|
||||
thread_header.push_str(&format!(
|
||||
"\n\n- issue {issue} found at file {}",
|
||||
path
|
||||
));
|
||||
thread_header
|
||||
.push_str(&format!("\n\n- issue {issue} found at file {path}"));
|
||||
}
|
||||
|
||||
if trace.is_empty() {
|
||||
|
||||
@@ -247,7 +247,7 @@ impl AuthProvider {
|
||||
state: String,
|
||||
) -> Result<String, AuthenticationError> {
|
||||
let self_addr = dotenvy::var("SELF_ADDR")?;
|
||||
let raw_redirect_uri = format!("{}/v2/auth/callback", self_addr);
|
||||
let raw_redirect_uri = format!("{self_addr}/v2/auth/callback");
|
||||
let redirect_uri = urlencoding::encode(&raw_redirect_uri);
|
||||
|
||||
Ok(match self {
|
||||
@@ -255,30 +255,24 @@ impl AuthProvider {
|
||||
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://github.com/login/oauth/authorize?client_id={}&prompt=select_account&state={}&scope=read%3Auser%20user%3Aemail&redirect_uri={}",
|
||||
client_id,
|
||||
state,
|
||||
redirect_uri,
|
||||
"https://github.com/login/oauth/authorize?client_id={client_id}&prompt=select_account&state={state}&scope=read%3Auser%20user%3Aemail&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Discord => {
|
||||
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?;
|
||||
|
||||
format!("https://discord.com/api/oauth2/authorize?client_id={}&state={}&response_type=code&scope=identify%20email&redirect_uri={}", client_id, state, redirect_uri)
|
||||
format!("https://discord.com/api/oauth2/authorize?client_id={client_id}&state={state}&response_type=code&scope=identify%20email&redirect_uri={redirect_uri}")
|
||||
}
|
||||
AuthProvider::Microsoft => {
|
||||
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?;
|
||||
|
||||
format!("https://login.live.com/oauth20_authorize.srf?client_id={}&response_type=code&scope=user.read&state={}&prompt=select_account&redirect_uri={}", client_id, state, redirect_uri)
|
||||
format!("https://login.live.com/oauth20_authorize.srf?client_id={client_id}&response_type=code&scope=user.read&state={state}&prompt=select_account&redirect_uri={redirect_uri}")
|
||||
}
|
||||
AuthProvider::GitLab => {
|
||||
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://gitlab.com/oauth/authorize?client_id={}&state={}&scope=read_user+profile+email&response_type=code&redirect_uri={}",
|
||||
client_id,
|
||||
state,
|
||||
redirect_uri,
|
||||
"https://gitlab.com/oauth/authorize?client_id={client_id}&state={state}&scope=read_user+profile+email&response_type=code&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Google => {
|
||||
@@ -342,8 +336,7 @@ impl AuthProvider {
|
||||
let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?;
|
||||
|
||||
let url = format!(
|
||||
"https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}&redirect_uri={}",
|
||||
client_id, client_secret, code, redirect_uri
|
||||
"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}"
|
||||
);
|
||||
|
||||
let token: AccessToken = reqwest::Client::new()
|
||||
@@ -482,9 +475,8 @@ impl AuthProvider {
|
||||
form.insert("openid.mode".to_string(), "check_authentication");
|
||||
|
||||
for val in signed.split(',') {
|
||||
if let Some(arr_val) = query.get(&format!("openid.{}", val))
|
||||
{
|
||||
form.insert(format!("openid.{}", val), &**arr_val);
|
||||
if let Some(arr_val) = query.get(&format!("openid.{val}")) {
|
||||
form.insert(format!("openid.{val}"), &**arr_val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,8 +613,7 @@ impl AuthProvider {
|
||||
email: discord_user.email,
|
||||
avatar_url: discord_user.avatar.map(|x| {
|
||||
format!(
|
||||
"https://cdn.discordapp.com/avatars/{}/{}.webp",
|
||||
id, x
|
||||
"https://cdn.discordapp.com/avatars/{id}/{x}.webp"
|
||||
)
|
||||
}),
|
||||
bio: None,
|
||||
@@ -741,9 +732,7 @@ impl AuthProvider {
|
||||
|
||||
let response: String = reqwest::get(
|
||||
&format!(
|
||||
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={}&steamids={}",
|
||||
api_key,
|
||||
token
|
||||
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={api_key}&steamids={token}"
|
||||
)
|
||||
)
|
||||
.await?
|
||||
@@ -1367,7 +1356,7 @@ pub async fn create_account_with_password(
|
||||
if let Some(feedback) =
|
||||
score.feedback().clone().and_then(|x| x.warning())
|
||||
{
|
||||
format!("Password too weak: {}", feedback)
|
||||
format!("Password too weak: {feedback}")
|
||||
} else {
|
||||
"Specified password is too weak! Please improve its strength."
|
||||
.to_string()
|
||||
@@ -2030,7 +2019,7 @@ pub async fn change_password(
|
||||
if let Some(feedback) =
|
||||
score.feedback().clone().and_then(|x| x.warning())
|
||||
{
|
||||
format!("Password too weak: {}", feedback)
|
||||
format!("Password too weak: {feedback}")
|
||||
} else {
|
||||
"Specified password is too weak! Please improve its strength.".to_string()
|
||||
},
|
||||
@@ -2085,8 +2074,8 @@ pub async fn change_password(
|
||||
|
||||
send_email(
|
||||
email,
|
||||
&format!("Password {}", changed),
|
||||
&format!("Your password has been {} on your account.", changed),
|
||||
&format!("Password {changed}"),
|
||||
&format!("Your password has been {changed} on your account."),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
|
||||
@@ -113,7 +113,7 @@ pub async fn create_pat(
|
||||
.take(60)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
let token = format!("mrp_{}", token);
|
||||
let token = format!("mrp_{token}");
|
||||
|
||||
let name = info.name.clone();
|
||||
database::models::pat_item::PersonalAccessToken {
|
||||
|
||||
@@ -116,7 +116,7 @@ pub async fn forge_updates(
|
||||
for game_version in &game_versions {
|
||||
response
|
||||
.promos
|
||||
.entry(format!("{}-recommended", game_version))
|
||||
.entry(format!("{game_version}-recommended"))
|
||||
.or_insert_with(|| version.version_number.clone());
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ pub async fn forge_updates(
|
||||
for game_version in &game_versions {
|
||||
response
|
||||
.promos
|
||||
.entry(format!("{}-latest", game_version))
|
||||
.entry(format!("{game_version}-latest"))
|
||||
.or_insert_with(|| version.version_number.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ where
|
||||
|
||||
match (
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={}", boundary).as_str(),
|
||||
format!("multipart/form-data; boundary={boundary}").as_str(),
|
||||
)
|
||||
.try_into_pair()
|
||||
{
|
||||
@@ -153,8 +153,7 @@ where
|
||||
}
|
||||
Err(err) => {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Error inserting test header: {:?}.",
|
||||
err
|
||||
"Error inserting test header: {err:?}."
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -424,7 +424,7 @@ pub async fn collection_icon_edit(
|
||||
|
||||
let collection_id: CollectionId = collection_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", collection_id),
|
||||
&format!("data/{collection_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@@ -393,7 +393,7 @@ pub async fn oauth_client_icon_edit(
|
||||
)
|
||||
.await?;
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", client_id),
|
||||
&format!("data/{client_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@@ -1096,7 +1096,7 @@ pub async fn organization_icon_edit(
|
||||
|
||||
let organization_id: OrganizationId = organization_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", organization_id),
|
||||
&format!("data/{organization_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@@ -676,8 +676,7 @@ pub async fn cancel_payout(
|
||||
.make_paypal_request::<(), ()>(
|
||||
Method::POST,
|
||||
&format!(
|
||||
"payments/payouts-item/{}/cancel",
|
||||
platform_id
|
||||
"payments/payouts-item/{platform_id}/cancel"
|
||||
),
|
||||
None,
|
||||
None,
|
||||
@@ -689,7 +688,7 @@ pub async fn cancel_payout(
|
||||
payouts
|
||||
.make_tremendous_request::<(), ()>(
|
||||
Method::POST,
|
||||
&format!("rewards/{}/cancel", platform_id),
|
||||
&format!("rewards/{platform_id}/cancel"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -810,8 +810,7 @@ async fn project_create_inner(
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'project' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'project' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -830,8 +829,7 @@ async fn project_create_inner(
|
||||
image_item::Image::clear_cache(image.id.into(), redis).await?;
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} does not exist",
|
||||
image_id
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,10 +710,8 @@ pub async fn project_edit(
|
||||
));
|
||||
}
|
||||
|
||||
let ids_to_delete = links
|
||||
.iter()
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
let ids_to_delete =
|
||||
links.keys().cloned().collect::<Vec<String>>();
|
||||
// Deletes all links from hashmap- either will be deleted or be replaced
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -1270,10 +1268,7 @@ pub async fn projects_edit(
|
||||
.await?;
|
||||
|
||||
if let Some(links) = &bulk_edit_project.link_urls {
|
||||
let ids_to_delete = links
|
||||
.iter()
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
let ids_to_delete = links.keys().cloned().collect::<Vec<String>>();
|
||||
// Deletes all links from hashmap- either will be deleted or be replaced
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -1482,7 +1477,7 @@ pub async fn project_icon_edit(
|
||||
|
||||
let project_id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", project_id),
|
||||
&format!("data/{project_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
@@ -1700,7 +1695,7 @@ pub async fn add_gallery_item(
|
||||
|
||||
let id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}/images", id),
|
||||
&format!("data/{id}/images"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(350),
|
||||
|
||||
@@ -178,8 +178,7 @@ pub async fn report_create(
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'report' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'report' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -198,8 +197,7 @@ pub async fn report_create(
|
||||
image_item::Image::clear_cache(image.id.into(), &redis).await?;
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} could not be found",
|
||||
image_id
|
||||
"Image {image_id} could not be found"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,8 +527,7 @@ pub async fn thread_send_message(
|
||||
) || image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'thread_message' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'thread_message' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -548,8 +547,7 @@ pub async fn thread_send_message(
|
||||
.await?;
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} does not exist",
|
||||
image_id
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +595,7 @@ pub async fn user_icon_edit(
|
||||
|
||||
let user_id: UserId = actual_user.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", user_id),
|
||||
&format!("data/{user_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@@ -486,8 +486,7 @@ async fn version_create_inner(
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'version' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'version' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -506,8 +505,7 @@ async fn version_create_inner(
|
||||
image_item::Image::clear_cache(image.id.into(), redis).await?;
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} does not exist",
|
||||
image_id
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -810,7 +808,7 @@ pub async fn upload_file(
|
||||
) -> Result<(), CreateError> {
|
||||
let (file_name, file_extension) = get_name_ext(content_disposition)?;
|
||||
|
||||
if other_file_names.contains(&format!("{}.{}", file_name, file_extension)) {
|
||||
if other_file_names.contains(&format!("{file_name}.{file_extension}")) {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!"
|
||||
.to_string(),
|
||||
|
||||
@@ -67,5 +67,5 @@ pub async fn push_back_user_expiry(
|
||||
}
|
||||
|
||||
fn get_field_name(user: UserId) -> String {
|
||||
format!("user_status:{}", user)
|
||||
format!("user_status:{user}")
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ impl AppendsMultipart for TestRequest {
|
||||
let (boundary, payload) = generate_multipart(data);
|
||||
self.append_header((
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={}", boundary),
|
||||
format!("multipart/form-data; boundary={boundary}"),
|
||||
))
|
||||
.set_payload(payload)
|
||||
}
|
||||
@@ -62,17 +62,12 @@ pub fn generate_multipart(
|
||||
|
||||
if let Some(filename) = &segment.filename {
|
||||
payload.extend_from_slice(
|
||||
format!("; filename=\"{filename}\"", filename = filename)
|
||||
.as_bytes(),
|
||||
format!("; filename=\"{filename}\"").as_bytes(),
|
||||
);
|
||||
}
|
||||
if let Some(content_type) = &segment.content_type {
|
||||
payload.extend_from_slice(
|
||||
format!(
|
||||
"\r\nContent-Type: {content_type}",
|
||||
content_type = content_type
|
||||
)
|
||||
.as_bytes(),
|
||||
format!("\r\nContent-Type: {content_type}").as_bytes(),
|
||||
);
|
||||
}
|
||||
payload.extend_from_slice(b"\r\n\r\n");
|
||||
@@ -87,9 +82,7 @@ pub fn generate_multipart(
|
||||
}
|
||||
payload.extend_from_slice(b"\r\n");
|
||||
}
|
||||
payload.extend_from_slice(
|
||||
format!("--{boundary}--\r\n", boundary = boundary).as_bytes(),
|
||||
);
|
||||
payload.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
|
||||
|
||||
(boundary, Bytes::from(payload))
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ pub async fn upload_image_optimized(
|
||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Invalid format for image: {}",
|
||||
file_extension
|
||||
"Invalid format for image: {file_extension}"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -91,7 +90,7 @@ pub async fn upload_image_optimized(
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("{}/{}.{}", upload_folder, hash, file_extension),
|
||||
&format!("{upload_folder}/{hash}.{file_extension}"),
|
||||
bytes,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -71,7 +71,7 @@ async fn get_webhook_metadata(
|
||||
url: format!(
|
||||
"{}/organization/{}",
|
||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||
organization.slug
|
||||
to_base62(organization.id.0 as u64)
|
||||
),
|
||||
icon_url: organization.icon_url,
|
||||
});
|
||||
@@ -97,7 +97,7 @@ async fn get_webhook_metadata(
|
||||
url: format!(
|
||||
"{}/user/{}",
|
||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||
user.username
|
||||
to_base62(user.id.0 as u64)
|
||||
),
|
||||
name: user.username,
|
||||
icon_url: user.avatar_url,
|
||||
@@ -145,11 +145,7 @@ async fn get_webhook_metadata(
|
||||
"{}/{}/{}",
|
||||
dotenvy::var("SITE_URL").unwrap_or_default(),
|
||||
project_type,
|
||||
project
|
||||
.inner
|
||||
.slug
|
||||
.clone()
|
||||
.unwrap_or_else(|| to_base62(project.inner.id.0 as u64))
|
||||
to_base62(project.inner.id.0 as u64)
|
||||
),
|
||||
project_title: project.inner.name,
|
||||
project_summary: project.inner.summary,
|
||||
|
||||
@@ -71,7 +71,7 @@ impl ApiV2 {
|
||||
};
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/search?{}{}", query_field, facets_field))
|
||||
.uri(&format!("/v2/search?{query_field}{facets_field}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@@ -99,7 +99,7 @@ impl ApiProject for ApiV2 {
|
||||
|
||||
// Approve as a moderator.
|
||||
let req = TestRequest::patch()
|
||||
.uri(&format!("/v2/project/{}", slug))
|
||||
.uri(&format!("/v2/project/{slug}"))
|
||||
.append_pat(MOD_USER_PAT)
|
||||
.set_json(json!(
|
||||
{
|
||||
@@ -114,7 +114,7 @@ impl ApiProject for ApiV2 {
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v2/project/{}/version", slug))
|
||||
.uri(&format!("/v2/project/{slug}/version"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@@ -217,7 +217,7 @@ impl ApiProject for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/user/{}/projects", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}/projects"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@@ -260,7 +260,7 @@ impl ApiProject for ApiV2 {
|
||||
) -> ServiceResponse {
|
||||
let projects_str = ids_or_slugs
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.map(|s| format!("\"{s}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let req = test::TestRequest::patch()
|
||||
@@ -490,13 +490,13 @@ impl ApiProject for ApiV2 {
|
||||
featured = featured
|
||||
);
|
||||
if let Some(title) = title {
|
||||
url.push_str(&format!("&title={}", title));
|
||||
url.push_str(&format!("&title={title}"));
|
||||
}
|
||||
if let Some(description) = description {
|
||||
url.push_str(&format!("&description={}", description));
|
||||
url.push_str(&format!("&description={description}"));
|
||||
}
|
||||
if let Some(ordering) = ordering {
|
||||
url.push_str(&format!("&ordering={}", ordering));
|
||||
url.push_str(&format!("&ordering={ordering}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
@@ -542,10 +542,7 @@ impl ApiProject for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v2/project/{id_or_slug}/gallery?url={url}",
|
||||
url = url
|
||||
))
|
||||
.uri(&format!("/v2/project/{id_or_slug}/gallery?url={url}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ impl ApiUser for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@@ -32,7 +32,7 @@ impl ApiUser for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
@@ -46,7 +46,7 @@ impl ApiUser for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
|
||||
@@ -399,18 +399,18 @@ impl ApiVersion for ApiV2 {
|
||||
));
|
||||
}
|
||||
if let Some(featured) = featured {
|
||||
query_string.push_str(&format!("&featured={}", featured));
|
||||
query_string.push_str(&format!("&featured={featured}"));
|
||||
}
|
||||
if let Some(version_type) = version_type {
|
||||
query_string.push_str(&format!("&version_type={}", version_type));
|
||||
query_string.push_str(&format!("&version_type={version_type}"));
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
let limit = limit.to_string();
|
||||
query_string.push_str(&format!("&limit={}", limit));
|
||||
query_string.push_str(&format!("&limit={limit}"));
|
||||
}
|
||||
if let Some(offset) = offset {
|
||||
let offset = offset.to_string();
|
||||
query_string.push_str(&format!("&offset={}", offset));
|
||||
query_string.push_str(&format!("&offset={offset}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
@@ -480,7 +480,7 @@ impl ApiVersion for ApiV2 {
|
||||
) -> ServiceResponse {
|
||||
let ids = url_encode_json_serialized_vec(&version_ids);
|
||||
let request = test::TestRequest::get()
|
||||
.uri(&format!("/v2/versions?ids={}", ids))
|
||||
.uri(&format!("/v2/versions?ids={ids}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
|
||||
@@ -157,7 +157,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/collections", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}/collections"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
|
||||
@@ -104,8 +104,7 @@ impl ApiV3 {
|
||||
code: auth_code,
|
||||
redirect_uri: original_redirect_uri,
|
||||
client_id: serde_json::from_str(&format!(
|
||||
"\"{}\"",
|
||||
client_id
|
||||
"\"{client_id}\""
|
||||
))
|
||||
.unwrap(),
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> Vec<OAuthClient> {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/oauth_apps", user_id))
|
||||
.uri(&format!("/v3/user/{user_id}/oauth_apps"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@@ -62,7 +62,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/_internal/oauth/app/{}", client_id))
|
||||
.uri(&format!("/_internal/oauth/app/{client_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
@@ -93,7 +93,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::delete()
|
||||
.uri(&format!("/_internal/oauth/app/{}", client_id))
|
||||
.uri(&format!("/_internal/oauth/app/{client_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ impl ApiProject for ApiV3 {
|
||||
|
||||
// Approve as a moderator.
|
||||
let req = TestRequest::patch()
|
||||
.uri(&format!("/v3/project/{}", slug))
|
||||
.uri(&format!("/v3/project/{slug}"))
|
||||
.append_pat(MOD_USER_PAT)
|
||||
.set_json(json!(
|
||||
{
|
||||
@@ -69,7 +69,7 @@ impl ApiProject for ApiV3 {
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/project/{}/version", slug))
|
||||
.uri(&format!("/v3/project/{slug}/version"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@@ -172,7 +172,7 @@ impl ApiProject for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/projects", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}/projects"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@@ -215,7 +215,7 @@ impl ApiProject for ApiV3 {
|
||||
) -> ServiceResponse {
|
||||
let projects_str = ids_or_slugs
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.map(|s| format!("\"{s}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let req = test::TestRequest::patch()
|
||||
@@ -363,13 +363,13 @@ impl ApiProject for ApiV3 {
|
||||
featured = featured
|
||||
);
|
||||
if let Some(title) = title {
|
||||
url.push_str(&format!("&title={}", title));
|
||||
url.push_str(&format!("&title={title}"));
|
||||
}
|
||||
if let Some(description) = description {
|
||||
url.push_str(&format!("&description={}", description));
|
||||
url.push_str(&format!("&description={description}"));
|
||||
}
|
||||
if let Some(ordering) = ordering {
|
||||
url.push_str(&format!("&ordering={}", ordering));
|
||||
url.push_str(&format!("&ordering={ordering}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
@@ -416,10 +416,7 @@ impl ApiProject for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/project/{id_or_slug}/gallery?url={url}",
|
||||
url = url
|
||||
))
|
||||
.uri(&format!("/v3/project/{id_or_slug}/gallery?url={url}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
@@ -562,7 +559,7 @@ impl ApiV3 {
|
||||
};
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/search?{}{}", query_field, facets_field))
|
||||
.uri(&format!("/v3/search?{query_field}{facets_field}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@@ -583,12 +580,12 @@ impl ApiV3 {
|
||||
let version_string: String =
|
||||
serde_json::to_string(&id_or_slugs).unwrap();
|
||||
let version_string = urlencoding::encode(&version_string);
|
||||
format!("version_ids={}", version_string)
|
||||
format!("version_ids={version_string}")
|
||||
} else {
|
||||
let projects_string: String =
|
||||
serde_json::to_string(&id_or_slugs).unwrap();
|
||||
let projects_string = urlencoding::encode(&projects_string);
|
||||
format!("project_ids={}", projects_string)
|
||||
format!("project_ids={projects_string}")
|
||||
};
|
||||
|
||||
let mut extra_args = String::new();
|
||||
@@ -605,10 +602,8 @@ impl ApiV3 {
|
||||
extra_args.push_str(&format!("&end_date={end_date}"));
|
||||
}
|
||||
if let Some(resolution_minutes) = resolution_minutes {
|
||||
extra_args.push_str(&format!(
|
||||
"&resolution_minutes={}",
|
||||
resolution_minutes
|
||||
));
|
||||
extra_args
|
||||
.push_str(&format!("&resolution_minutes={resolution_minutes}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
|
||||
@@ -76,7 +76,7 @@ impl ApiV3 {
|
||||
loader_field: &str,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/loader_field?loader_field={}", loader_field))
|
||||
.uri(&format!("/v3/loader_field?loader_field={loader_field}"))
|
||||
.append_pat(ADMIN_USER_PAT)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
|
||||
@@ -13,7 +13,7 @@ impl ApiUser for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@@ -34,7 +34,7 @@ impl ApiUser for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
@@ -48,7 +48,7 @@ impl ApiUser for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
|
||||
@@ -432,18 +432,18 @@ impl ApiVersion for ApiV3 {
|
||||
));
|
||||
}
|
||||
if let Some(featured) = featured {
|
||||
query_string.push_str(&format!("&featured={}", featured));
|
||||
query_string.push_str(&format!("&featured={featured}"));
|
||||
}
|
||||
if let Some(version_type) = version_type {
|
||||
query_string.push_str(&format!("&version_type={}", version_type));
|
||||
query_string.push_str(&format!("&version_type={version_type}"));
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
let limit = limit.to_string();
|
||||
query_string.push_str(&format!("&limit={}", limit));
|
||||
query_string.push_str(&format!("&limit={limit}"));
|
||||
}
|
||||
if let Some(offset) = offset {
|
||||
let offset = offset.to_string();
|
||||
query_string.push_str(&format!("&offset={}", offset));
|
||||
query_string.push_str(&format!("&offset={offset}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
@@ -513,7 +513,7 @@ impl ApiVersion for ApiV3 {
|
||||
) -> ServiceResponse {
|
||||
let ids = url_encode_json_serialized_vec(&version_ids);
|
||||
let request = test::TestRequest::get()
|
||||
.uri(&format!("/v3/versions?ids={}", ids))
|
||||
.uri(&format!("/v3/versions?ids={ids}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
@@ -546,10 +546,7 @@ impl ApiVersion for ApiV3 {
|
||||
Some(file),
|
||||
);
|
||||
let request = test::TestRequest::post()
|
||||
.uri(&format!(
|
||||
"/v3/version/{version_id}/file",
|
||||
version_id = version_id
|
||||
))
|
||||
.uri(&format!("/v3/version/{version_id}/file"))
|
||||
.append_pat(pat)
|
||||
.set_multipart(m)
|
||||
.to_request();
|
||||
@@ -562,10 +559,7 @@ impl ApiVersion for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let request = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/version/{version_id}",
|
||||
version_id = version_id
|
||||
))
|
||||
.uri(&format!("/v3/version/{version_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
|
||||
@@ -137,7 +137,7 @@ impl TemporaryDatabase {
|
||||
dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
let mut template_url =
|
||||
Url::parse(&url).expect("Invalid database URL");
|
||||
template_url.set_path(&format!("/{}", TEMPLATE_DATABASE_NAME));
|
||||
template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}"));
|
||||
|
||||
let pool = PgPool::connect(template_url.as_str())
|
||||
.await
|
||||
|
||||
@@ -470,8 +470,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != ProjectPermissions::empty() {
|
||||
return Err(format!(
|
||||
"Test 1 failed. Expected no permissions, got {:?}",
|
||||
p
|
||||
"Test 1 failed. Expected no permissions, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -511,8 +510,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != ProjectPermissions::empty() {
|
||||
return Err(format!(
|
||||
"Test 2 failed. Expected no permissions, got {:?}",
|
||||
p
|
||||
"Test 2 failed. Expected no permissions, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -561,8 +559,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_project_permissions {
|
||||
return Err(format!(
|
||||
"Test 3 failed. Expected {:?}, got {:?}",
|
||||
failure_project_permissions, p
|
||||
"Test 3 failed. Expected {failure_project_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -607,8 +604,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 4 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 4 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -666,8 +662,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_project_permissions {
|
||||
return Err(format!(
|
||||
"Test 5 failed. Expected {:?}, got {:?}",
|
||||
failure_project_permissions, p
|
||||
"Test 5 failed. Expected {failure_project_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -721,8 +716,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 6 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 6 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -790,8 +784,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_project_permissions {
|
||||
return Err(format!(
|
||||
"Test 7 failed. Expected {:?}, got {:?}",
|
||||
failure_project_permissions, p
|
||||
"Test 7 failed. Expected {failure_project_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -856,8 +849,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 8 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 8 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -927,8 +919,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != OrganizationPermissions::empty() {
|
||||
return Err(format!(
|
||||
"Test 1 failed. Expected no permissions, got {:?}",
|
||||
p
|
||||
"Test 1 failed. Expected no permissions, got {p:?}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
@@ -976,8 +967,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_organization_permissions {
|
||||
return Err(format!(
|
||||
"Test 2 failed. Expected {:?}, got {:?}",
|
||||
failure_organization_permissions, p
|
||||
"Test 2 failed. Expected {failure_organization_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
@@ -1021,8 +1011,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 3 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 3 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -487,7 +487,7 @@ async fn test_multi_get_redis_cache() {
|
||||
// Create 5 modpacks
|
||||
let mut modpacks = Vec::new();
|
||||
for i in 0..5 {
|
||||
let slug = format!("test-modpack-{}", i);
|
||||
let slug = format!("test-modpack-{i}");
|
||||
|
||||
let creation_data = get_public_project_creation_data(
|
||||
&slug,
|
||||
@@ -503,7 +503,7 @@ async fn test_multi_get_redis_cache() {
|
||||
// Create 5 mods
|
||||
let mut mods = Vec::new();
|
||||
for i in 0..5 {
|
||||
let slug = format!("test-mod-{}", i);
|
||||
let slug = format!("test-mod-{i}");
|
||||
|
||||
let creation_data = get_public_project_creation_data(
|
||||
&slug,
|
||||
|
||||
@@ -28,7 +28,7 @@ async fn oauth_flow_happy_path() {
|
||||
} = &env.dummy.oauth_client_alpha;
|
||||
|
||||
// Initiate authorization
|
||||
let redirect_uri = format!("{}?foo=bar", base_redirect_uri);
|
||||
let redirect_uri = format!("{base_redirect_uri}?foo=bar");
|
||||
let original_state = "1234";
|
||||
let resp = env
|
||||
.api
|
||||
|
||||
@@ -81,7 +81,7 @@ pub async fn pat_full_test() {
|
||||
|
||||
// Change scopes and test again
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": 0,
|
||||
@@ -93,7 +93,7 @@ pub async fn pat_full_test() {
|
||||
|
||||
// Change scopes back, and set expiry to the past, and test again
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": Scopes::COLLECTION_CREATE,
|
||||
@@ -109,21 +109,21 @@ pub async fn pat_full_test() {
|
||||
|
||||
// Change everything back to normal and test again
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() + Duration::days(1), // no longer expired!
|
||||
}))
|
||||
.to_request();
|
||||
|
||||
println!("PAT ID FOR TEST: {}", id);
|
||||
println!("PAT ID FOR TEST: {id}");
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
assert_eq!(mock_pat_test(access_token).await, 200); // Works again
|
||||
|
||||
// Patching to a bad expiry should fail
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() - Duration::days(1), // Past
|
||||
@@ -140,7 +140,7 @@ pub async fn pat_full_test() {
|
||||
}
|
||||
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": scope.bits(),
|
||||
@@ -156,7 +156,7 @@ pub async fn pat_full_test() {
|
||||
// Delete PAT
|
||||
let req = test::TestRequest::delete()
|
||||
.append_pat(USER_USER_PAT)
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
@@ -260,7 +260,7 @@ pub async fn bad_pats() {
|
||||
|
||||
// Patching to a bad expiry should fail
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() - Duration::days(1), // Past
|
||||
@@ -277,7 +277,7 @@ pub async fn bad_pats() {
|
||||
}
|
||||
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": scope.bits(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user