MR App 0.9.5 - Big bugfix update (#3585)

* Add launcher_feature_version to Profile

* Misc fixes

- Add typing to theme and settings stuff
- Push instance route on creation from installing a modpack
- Fixed servers not reloading properly when first added

* Make old instances scan the logs folder for joined servers on launcher startup

* Create AttachedWorldData

* Change AttachedWorldData interface

* Rename WorldType::World to WorldType::Singleplayer

* Implement world display status system

* Fix Minecraft font

* Fix set_world_display_status Tauri error

* Add 'Play instance' option

* Add option to disable worlds showing in Home

* Fixes

- Fix available server filter only showing if there are some available
- Fixed server and singleplayer filters sometimes showing when there are only servers or singleplayer worlds
- Fixed new worlds not being automatically added when detected
- Rephrased Jump back into worlds option description

* Fixed sometimes more than 6 items showing up in Jump back in

* Fix servers.dat issue with instances you haven't played before

* Fix too large of bulk requests being made, limit max to 800 #3430

* Add hiding from home page, add types to Mods.vue

* Make recent worlds go into grid when display is huge

* Fix lint

* Remove redundant media query

* Fix protocol version on home page, and home page being blocked by pinging servers

* Clippy fix

* More Clippy fixes

* Fix Prettier lints

* Undo `from_string` changes

---------

Co-authored-by: Josiah Glosson <soujournme@gmail.com>
Co-authored-by: Alejandro González <me@alegon.dev>
This commit is contained in:
Prospector
2025-05-01 16:13:13 -07:00
committed by GitHub
parent 4a2605bc1e
commit 3dad6b317f
123 changed files with 1622 additions and 744 deletions

26
Cargo.lock generated
View File

@@ -2604,6 +2604,28 @@ dependencies = [
"syn 2.0.90", "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]] [[package]]
name = "env_filter" name = "env_filter"
version = "0.1.3" version = "0.1.3"
@@ -9239,6 +9261,7 @@ name = "theseus"
version = "0.9.4" version = "0.9.4"
dependencies = [ dependencies = [
"ariadne", "ariadne",
"async-compression",
"async-recursion", "async-recursion",
"async-tungstenite", "async-tungstenite",
"async-walkdir", "async-walkdir",
@@ -9253,6 +9276,7 @@ dependencies = [
"discord-rich-presence", "discord-rich-presence",
"dunce", "dunce",
"either", "either",
"enumset",
"flate2", "flate2",
"fs4", "fs4",
"futures", "futures",
@@ -9300,6 +9324,7 @@ dependencies = [
"daedalus", "daedalus",
"dashmap 6.1.0", "dashmap 6.1.0",
"either", "either",
"enumset",
"native-dialog", "native-dialog",
"objc", "objc",
"opener", "opener",
@@ -9333,6 +9358,7 @@ dependencies = [
name = "theseus_playground" name = "theseus_playground"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"enumset",
"theseus", "theseus",
"tokio", "tokio",
"tracing", "tracing",

View File

@@ -24,7 +24,7 @@ import { useLoading, useTheming } from '@/store/state'
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component' import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
import AccountsCard from '@/components/ui/AccountsCard.vue' import AccountsCard from '@/components/ui/AccountsCard.vue'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.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 Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue' import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue' import SplashScreen from '@/components/ui/SplashScreen.vue'

View File

@@ -7,7 +7,7 @@
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 400; 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 { @font-face {
@@ -15,7 +15,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
font-weight: 400; 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 { @font-face {
@@ -23,7 +23,7 @@
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 600; 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 { @font-face {
@@ -31,7 +31,7 @@
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
font-weight: 600; 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 { .font-minecraft {

View File

@@ -14,7 +14,7 @@ import { ref, computed } from 'vue'
import { login as login_flow, set_default_user } from '@/helpers/auth.js' import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { handleSevereError } from '@/store/error.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 { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'

View File

@@ -1,5 +1,5 @@
<template> <template>
<ModalWrapper ref="modal" header="Create instance"> <ModalWrapper ref="modal" header="Creating an instance">
<div class="modal-header"> <div class="modal-header">
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" /> <Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
</div> </div>

View File

@@ -124,8 +124,11 @@ import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { install as installVersion } from '@/store/install.js' import { install as installVersion } from '@/store/install.js'
import { useRouter } from 'vue-router'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const router = useRouter()
const props = defineProps({ const props = defineProps({
backgroundImage: { backgroundImage: {
type: String, type: String,
@@ -168,6 +171,9 @@ async function install() {
installing.value = false installing.value = false
emit('install', props.project.project_id ?? props.project.id) emit('install', props.project.project_id ?? props.project.id)
}, },
(profile) => {
router.push(`/instance/${profile}`)
},
) )
} }

View File

@@ -13,15 +13,17 @@ const confirmModal = ref(null)
const installing = ref(false) const installing = ref(false)
const onInstall = ref(() => {}) const onInstall = ref(() => {})
const onCreateInstance = ref(() => {})
defineExpose({ defineExpose({
show: (projectVal, versionIdVal, callback) => { show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
project.value = projectVal project.value = projectVal
versionId.value = versionIdVal versionId.value = versionIdVal
installing.value = false installing.value = false
confirmModal.value.show() confirmModal.value.show()
onInstall.value = callback onInstall.value = callback
onCreateInstance.value = createInstanceCallback
trackEvent('PackInstallStart') trackEvent('PackInstallStart')
}, },
@@ -36,6 +38,7 @@ async function install() {
versionId.value, versionId.value,
project.value.title, project.value.title,
project.value.icon_url, project.value.icon_url,
onCreateInstance.value,
).catch(handleError) ).catch(handleError)
trackEvent('PackInstall', { trackEvent('PackInstall', {
id: project.value.id, id: project.value.id,

View File

@@ -3,7 +3,7 @@ import { Checkbox } from '@modrinth/ui'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { handleError } from '@/store/notifications' import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl' import { defineMessages, useVIntl } from '@vintl/vintl'
import { get } from '@/helpers/settings' import { get } from '@/helpers/settings.ts'
import { edit } from '@/helpers/profile' import { edit } from '@/helpers/profile'
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types' import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'

View File

@@ -7,7 +7,7 @@ import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl' import { defineMessages, useVIntl } from '@vintl/vintl'
import JavaSelector from '@/components/ui/JavaSelector.vue' import JavaSelector from '@/components/ui/JavaSelector.vue'
import { get_max_memory } from '@/helpers/jre' 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' import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()

View File

@@ -3,7 +3,7 @@ import { Checkbox, Toggle } from '@modrinth/ui'
import { computed, ref, type Ref, watch } from 'vue' import { computed, ref, type Ref, watch } from 'vue'
import { handleError } from '@/store/notifications' import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl' import { defineMessages, useVIntl } from '@vintl/vintl'
import { get } from '@/helpers/settings' import { get } from '@/helpers/settings.ts'
import { edit } from '@/helpers/profile' import { edit } from '@/helpers/profile'
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types' import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'

View File

@@ -22,7 +22,7 @@ import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/
import { useTheming } from '@/store/state' import { useTheming } from '@/store/state'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue' import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.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() const themeStore = useTheming()

View File

@@ -2,7 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { ConfirmModal } from '@modrinth/ui' import { ConfirmModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js' 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() const themeStore = useTheming()

View File

@@ -2,7 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { NewModal as Modal } from '@modrinth/ui' import { NewModal as Modal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js' 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() const themeStore = useTheming()

View File

@@ -2,7 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { ShareModal } from '@modrinth/ui' import { ShareModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js' 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() const themeStore = useTheming()

View File

@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui' import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
import { useTheming } from '@/store/state' import { useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings.ts'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { getOS } from '@/helpers/utils' import { getOS } from '@/helpers/utils'
import type { ColorTheme } from '@/store/theme.ts'
const themeStore = useTheming() const themeStore = useTheming()
@@ -24,13 +25,13 @@ watch(
<ThemeSelector <ThemeSelector
:update-color-theme=" :update-color-theme="
(theme) => { (theme: ColorTheme) => {
themeStore.setThemeState(theme) themeStore.setThemeState(theme)
settings.theme = theme settings.theme = theme
} }
" "
:current-theme="settings.theme" :current-theme="settings.theme"
:theme-options="themeStore.themeOptions" :theme-options="themeStore.getThemeOptions()"
system-theme-color="system" system-theme-color="system"
/> />
@@ -80,10 +81,28 @@ watch(
id="opening-page" id="opening-page"
v-model="settings.default_page" v-model="settings.default_page"
name="Opening page dropdown" name="Opening page dropdown"
class="w-40"
:options="['Home', 'Library']" :options="['Home', 'Library']"
/> />
</div> </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 class="mt-4 flex items-center justify-between">
<div> <div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2> <h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings.ts'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { get_max_memory } from '@/helpers/jre' import { get_max_memory } from '@/helpers/jre'
import { handleError } from '@/store/notifications' import { handleError } from '@/store/notifications'

View File

@@ -2,18 +2,15 @@
import { Toggle } from '@modrinth/ui' import { Toggle } from '@modrinth/ui'
import { useTheming } from '@/store/state' import { useTheming } from '@/store/state'
import { ref, watch } from 'vue' 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 themeStore = useTheming()
const settings = ref(await get()) const settings = ref(await getSettings())
const options = ref(['project_background', 'page_path', 'worlds_tab']) const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
function getStoreValue(key: string) { function setFeatureFlag(key: string, value: boolean) {
return themeStore.featureFlags[key] ?? false
}
function setStoreValue(key: string, value: boolean) {
themeStore.featureFlags[key] = value themeStore.featureFlags[key] = value
settings.value.feature_flags[key] = value settings.value.feature_flags[key] = value
} }
@@ -21,7 +18,7 @@ function setStoreValue(key: string, value: boolean) {
watch( watch(
settings, settings,
async () => { async () => {
await set(settings.value) await setSettings(settings.value)
}, },
{ deep: true }, { deep: true },
) )
@@ -36,8 +33,8 @@ watch(
<Toggle <Toggle
id="advanced-rendering" id="advanced-rendering"
:model-value="getStoreValue(option)" :model-value="themeStore.getFeatureFlag(option)"
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])" @update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
/> />
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings.ts'
import { Toggle } from '@modrinth/ui' import { Toggle } from '@modrinth/ui'
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics' import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { Button, Slider } from '@modrinth/ui' import { Button, Slider } from '@modrinth/ui'
import { ref, watch } from 'vue' 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 { purge_cache_types } from '@/helpers/cache.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets' import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'

View File

@@ -153,7 +153,7 @@ onUnmounted(() => {
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary"> <span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
<router-link <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}`" :to="`/project/${modpack.id}`"
> >
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" /> <Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />

View File

@@ -13,16 +13,17 @@ import {
import { HeadingLink, GAME_MODES } from '@modrinth/ui' import { HeadingLink, GAME_MODES } from '@modrinth/ui'
import WorldItem from '@/components/ui/world/WorldItem.vue' import WorldItem from '@/components/ui/world/WorldItem.vue'
import InstanceItem from '@/components/ui/world/InstanceItem.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 type { Dayjs } from 'dayjs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useTheming } from '@/store/theme' import { useTheming } from '@/store/theme.ts'
import { kill } from '@/helpers/profile' import { kill, run } from '@/helpers/profile'
import { handleError } from '@/store/notifications' import { handleError } from '@/store/notifications'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { process_listener, profile_listener } from '@/helpers/events' import { process_listener, profile_listener } from '@/helpers/events'
import { get_all } from '@/helpers/process' import { get_all } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import { handleSevereError } from '@/store/error'
const props = defineProps<{ const props = defineProps<{
recentInstances: GameInstance[] recentInstances: GameInstance[]
@@ -54,7 +55,9 @@ type WorldJumpBackInItem = BaseJumpBackInItem & {
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem 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(() => { await populateJumpBackIn().catch(() => {
console.error('Failed to populate jump back in') console.error('Failed to populate jump back in')
}) })
@@ -66,66 +69,71 @@ await populateJumpBackIn().catch(() => {
async function populateJumpBackIn() { async function populateJumpBackIn() {
console.info('Repopulating jump back in...') console.info('Repopulating jump back in...')
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN)
const worldItems: WorldJumpBackInItem[] = [] const worldItems: WorldJumpBackInItem[] = []
worlds.forEach((world) => {
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
if (!instance || !world.last_played) { if (showWorlds.value) {
return const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
}
worldItems.push({ worlds.forEach((world) => {
type: 'world', const instance = props.recentInstances.find((instance) => instance.path === world.profile)
last_played: dayjs(world.last_played),
world: world,
instance: instance,
})
})
const servers: { if (!instance || !world.last_played) {
instancePath: string return
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 worldItems.push({
await Promise.all( type: 'world',
servers.map(({ instancePath, address }) => last_played: dayjs(world.last_played),
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address), 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[] = [] const instanceItems: InstanceJumpBackInItem[] = []
props.recentInstances.forEach((instance) => { for (const instance of props.recentInstances) {
if (worldItems.some((item) => item.instance.path === instance.path) || !instance.last_played) { const worldItem = worldItems.find((item) => item.instance.path === instance.path)
return if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
continue
} }
instanceItems.push({ instanceItems.push({
@@ -133,13 +141,13 @@ async function populateJumpBackIn() {
last_played: dayjs(instance.last_played), last_played: dayjs(instance.last_played),
instance: instance, instance: instance,
}) })
}) }
const items: JumpBackInItem[] = [...worldItems, ...instanceItems] const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played))) items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
jumpBackInItems.value = items.filter( jumpBackInItems.value = items
(item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO), .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) { 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) { async function stopInstance(path: string) {
await kill(path).catch(handleError) await kill(path).catch(handleError)
trackEvent('InstanceStop', { trackEvent('InstanceStop', {
@@ -209,11 +229,7 @@ onUnmounted(() => {
<template> <template>
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2"> <div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
<HeadingLink <HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
v-if="(theme.featureFlags as Record<string, boolean>)['worlds_tab']"
to="/worlds"
class="mt-1"
>
Jump back in Jump back in
</HeadingLink> </HeadingLink>
<span <span
@@ -222,7 +238,7 @@ onUnmounted(() => {
> >
Jump back in Jump back in
</span> </span>
<div class="flex flex-col w-full gap-2"> <div class="grid-when-huge flex flex-col w-full gap-2">
<template <template
v-for="item in jumpBackInItems" v-for="item in jumpBackInItems"
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`" :key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
@@ -246,7 +262,7 @@ onUnmounted(() => {
:rendered-motd=" :rendered-motd="
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined 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=" :game-mode="
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
" "
@@ -259,6 +275,7 @@ onUnmounted(() => {
? refreshServer(item.world.address, item.instance.path) ? refreshServer(item.world.address, item.instance.path)
: {} : {}
" "
@update="() => populateJumpBackIn()"
@play=" @play="
() => { () => {
currentProfile = item.instance.path currentProfile = item.instance.path
@@ -266,6 +283,12 @@ onUnmounted(() => {
joinWorld(item.world) joinWorld(item.world)
} }
" "
@play-instance="
() => {
currentProfile = item.instance.path
playInstance(item.instance)
}
"
@stop="() => stopInstance(item.instance.path)" @stop="() => stopInstance(item.instance.path)"
/> />
<InstanceItem v-else :instance="item.instance" /> <InstanceItem v-else :instance="item.instance" />
@@ -273,3 +296,9 @@ onUnmounted(() => {
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss">
.grid-when-huge {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
}
</style>

View File

@@ -1,7 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts' 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 { formatNumber } from '@modrinth/utils'
import { import {
IssuesIcon, IssuesIcon,
@@ -19,6 +23,7 @@ import {
TrashIcon, TrashIcon,
UpdatedIcon, UpdatedIcon,
UserIcon, UserIcon,
XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui' import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
import type { MessageDescriptor } from '@vintl/vintl' import type { MessageDescriptor } from '@vintl/vintl'
@@ -35,7 +40,7 @@ const { formatMessage } = useVIntl()
const router = useRouter() const router = useRouter()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'play' | 'stop' | 'refresh' | 'edit' | 'delete'): void (e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
}>() }>()
const props = withDefaults( const props = withDefaults(
@@ -69,6 +74,7 @@ const props = withDefaults(
playingWorld: false, playingWorld: false,
startingInstance: false, startingInstance: false,
supportsQuickPlay: false, supportsQuickPlay: false,
currentProtocol: null,
refreshing: false, refreshing: false,
serverStatus: undefined, serverStatus: undefined,
@@ -143,10 +149,18 @@ const messages = defineMessages({
id: 'instance.worlds.play_anyway', id: 'instance.worlds.play_anyway',
defaultMessage: 'Play anyway', defaultMessage: 'Play anyway',
}, },
playInstance: {
id: 'instance.worlds.play_instance',
defaultMessage: 'Play instance',
},
worldInUse: { worldInUse: {
id: 'instance.worlds.world_in_use', id: 'instance.worlds.world_in_use',
defaultMessage: 'World is in use', defaultMessage: 'World is in use',
}, },
dontShowOnHome: {
id: 'instance.worlds.dont_show_on_home',
defaultMessage: `Don't show on Home`,
},
}) })
</script> </script>
<template> <template>
@@ -336,6 +350,12 @@ const messages = defineMessages({
<ButtonStyled circular type="transparent"> <ButtonStyled circular type="transparent">
<OverflowMenu <OverflowMenu
:options="[ :options="[
{
id: 'play-instance',
shown: !!instancePath,
disabled: playingInstance,
action: () => emit('play-instance'),
},
{ {
id: 'play-anyway', id: 'play-anyway',
shown: serverIncompatible && !playingInstance && supportsQuickPlay, shown: serverIncompatible && !playingInstance && supportsQuickPlay,
@@ -344,7 +364,7 @@ const messages = defineMessages({
{ {
id: 'open-instance', id: 'open-instance',
shown: !!instancePath, shown: !!instancePath,
action: () => router.push(encodeURI(`/instance/${instancePath}/worlds`)), action: () => router.push(encodeURI(`/instance/${instancePath}`)),
}, },
{ {
id: 'refresh', id: 'refresh',
@@ -369,6 +389,24 @@ const messages = defineMessages({
action: () => action: () =>
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {}, 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, divider: true,
shown: !instancePath, shown: !instancePath,
@@ -385,6 +423,10 @@ const messages = defineMessages({
]" ]"
> >
<MoreVerticalIcon aria-hidden="true" /> <MoreVerticalIcon aria-hidden="true" />
<template #play-instance>
<PlayIcon aria-hidden="true" />
{{ formatMessage(messages.playInstance) }}
</template>
<template #play-anyway> <template #play-anyway>
<PlayIcon aria-hidden="true" /> <PlayIcon aria-hidden="true" />
{{ formatMessage(messages.playAnyway) }} {{ formatMessage(messages.playAnyway) }}
@@ -406,6 +448,10 @@ const messages = defineMessages({
<template #refresh> <template #refresh>
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }} <UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
</template> </template>
<template #dont-show-on-home>
<XIcon aria-hidden="true" />
{{ formatMessage(messages.dontShowOnHome) }}
</template>
<template #delete> <template #delete>
<TrashIcon aria-hidden="true" /> <TrashIcon aria-hidden="true" />
{{ {{

View File

@@ -1,13 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { SaveIcon, XIcon } from '@modrinth/assets' import { SaveIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, commonMessages } from '@modrinth/ui' import { ButtonStyled, commonMessages } from '@modrinth/ui'
import { ref } from 'vue' import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import type { GameInstance } from '@/helpers/types' 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 { defineMessage, useVIntl } from '@vintl/vintl'
import { handleError } from '@/store/notifications' import { handleError } from '@/store/notifications'
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue' import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -21,10 +28,14 @@ const props = defineProps<{
const modal = ref() const modal = ref()
const name = ref() const name = ref<string>('')
const address = ref() const address = ref<string>('')
const resourcePack = ref('enabled') const resourcePack = ref<ServerPackStatus>('enabled')
const index = ref() const index = ref<number>(0)
const displayStatus = ref<DisplayStatus>('normal')
const hideFromHome = ref(false)
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
async function saveServer() { async function saveServer() {
const serverName = name.value ? name.value : address.value const serverName = name.value ? name.value : address.value
@@ -36,12 +47,23 @@ async function saveServer() {
address.value, address.value,
resourcePackStatus, resourcePackStatus,
).catch(handleError) ).catch(handleError)
if (newDisplayStatus.value !== displayStatus.value) {
await set_world_display_status(
props.instance.path,
'server',
address.value,
newDisplayStatus.value,
)
}
emit('submit', { emit('submit', {
name: serverName, name: serverName,
type: 'server', type: 'server',
index: index.value, index: index.value,
address: address.value, address: address.value,
pack_status: resourcePackStatus, pack_status: resourcePackStatus,
display_status: newDisplayStatus.value,
}) })
hide() hide()
} }
@@ -51,6 +73,8 @@ function show(server: ServerWorld) {
address.value = server.address address.value = server.address
resourcePack.value = server.pack_status resourcePack.value = server.pack_status
index.value = server.index index.value = server.index
displayStatus.value = server.display_status
hideFromHome.value = server.display_status === 'hidden'
modal.value.show() modal.value.show()
} }
@@ -75,6 +99,7 @@ const titleMessage = defineMessage({
v-model:address="address" v-model:address="address"
v-model:resource-pack="resourcePack" v-model:resource-pack="resourcePack"
/> />
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
<div class="flex gap-2 mt-4"> <div class="flex gap-2 mt-4">
<ButtonStyled color="brand"> <ButtonStyled color="brand">
<button :disabled="!address" @click="saveServer"> <button :disabled="!address" @click="saveServer">

View File

@@ -1,18 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets' import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui' 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 ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import type { GameInstance } from '@/helpers/types' import type { GameInstance } from '@/helpers/types'
import type { SingleplayerWorld } from '@/helpers/worlds.ts' import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
import { rename_world, reset_world_icon } 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 { defineMessages, useVIntl } from '@vintl/vintl'
import { handleError } from '@/store/notifications' import { handleError } from '@/store/notifications'
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const emit = defineEmits<{ const emit = defineEmits<{
submit: [path: string, name: string, removeIcon: boolean] submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
}>() }>()
const props = defineProps<{ const props = defineProps<{
@@ -25,6 +26,10 @@ const icon = ref()
const name = ref() const name = ref()
const path = ref() const path = ref()
const removeIcon = ref(false) const removeIcon = ref(false)
const displayStatus = ref<DisplayStatus>('normal')
const hideFromHome = ref(false)
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
async function saveWorld() { async function saveWorld() {
await rename_world(props.instance.path, path.value, name.value).catch(handleError) await rename_world(props.instance.path, path.value, name.value).catch(handleError)
@@ -32,8 +37,16 @@ async function saveWorld() {
if (removeIcon.value) { if (removeIcon.value) {
await reset_world_icon(props.instance.path, path.value).catch(handleError) 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() hide()
} }
@@ -41,6 +54,8 @@ function show(world: SingleplayerWorld) {
name.value = world.name name.value = world.name
path.value = world.path path.value = world.path
icon.value = world.icon icon.value = world.icon
displayStatus.value = world.display_status
hideFromHome.value = world.display_status === 'hidden'
modal.value.show() modal.value.show()
} }
@@ -87,6 +102,7 @@ const messages = defineMessages({
class="w-full" class="w-full"
autocomplete="off" autocomplete="off"
/> />
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
</div> </div>
<div class="flex gap-2 mt-4"> <div class="flex gap-2 mt-4">
<ButtonStyled color="brand"> <ButtonStyled color="brand">

View File

@@ -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>

View File

@@ -7,7 +7,13 @@ import { invoke } from '@tauri-apps/api/core'
import { create } from './profile' import { create } from './profile'
// Installs pack from a version ID // 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 = { const location = {
type: 'fromVersionId', type: 'fromVersionId',
project_id: projectId, project_id: projectId,
@@ -24,6 +30,7 @@ export async function create_profile_and_install(projectId, versionId, packTitle
null, null,
true, true,
) )
createInstanceCallback(profile)
return await invoke('plugin:pack|pack_install', { location, profile }) return await invoke('plugin:pack|pack_install', { location, profile })
} }

View File

@@ -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')
}

View 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')
}

View File

@@ -48,6 +48,32 @@ type LinkedData = {
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge' 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 = { type MemorySettings = {
maximum: number maximum: number
} }
@@ -88,6 +114,7 @@ type AppSettings = {
collapsed_navigation: boolean collapsed_navigation: boolean
advanced_rendering: boolean advanced_rendering: boolean
native_decorations: boolean native_decorations: boolean
worlds_in_home: boolean
telemetry: boolean telemetry: boolean
discord_rpc: boolean discord_rpc: boolean

View File

@@ -9,8 +9,13 @@ type BaseWorld = {
name: string name: string
last_played?: string last_played?: string
icon?: string icon?: string
display_status: DisplayStatus
type: WorldType
} }
export type WorldType = 'singleplayer' | 'server'
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
export type SingleplayerWorld = BaseWorld & { export type SingleplayerWorld = BaseWorld & {
type: 'singleplayer' type: 'singleplayer'
path: string path: string
@@ -70,8 +75,11 @@ export type ServerData = {
renderedMotd?: string renderedMotd?: string
} }
export async function get_recent_worlds(limit: number): Promise<WorldWithProfile[]> { export async function get_recent_worlds(
return await invoke('plugin:worlds|get_recent_worlds', { limit }) 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[]> { 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 }) 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( export async function rename_world(
instance: string, instance: string,
world: string, world: string,
@@ -230,12 +252,14 @@ export async function refreshServers(
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) { export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath) const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
const newWorld = await get_singleplayer_world(instancePath, worldPath)
if (index !== -1) { if (index !== -1) {
worlds[index] = await get_singleplayer_world(instancePath, worldPath) worlds[index] = newWorld
sortWorlds(worlds)
} else { } 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( export async function handleDefaultProfileUpdateEvent(

View File

@@ -22,7 +22,7 @@
<FilterIcon class="text-secondary h-5 w-5 mr-1" /> <FilterIcon class="text-secondary h-5 w-5 mr-1" />
<button <button
v-for="filter in filterOptions" 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'}`" :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)" @click="toggleArray(selectedFilters, filter.id)"
> >
@@ -47,7 +47,7 @@
path: x.path, path: x.path,
disabled: x.disabled, disabled: x.disabled,
filename: x.file_name, filename: x.file_name,
icon: x.icon, icon: x.icon ?? undefined,
title: x.name, title: x.name,
data: x, data: x,
} }
@@ -156,7 +156,7 @@
color-fill="text" color-fill="text"
hover-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 <DownloadIcon /> Update pack
</button> </button>
</ButtonStyled> </ButtonStyled>
@@ -170,7 +170,7 @@
> >
<button <button
v-tooltip="`Update`" v-tooltip="`Update`"
:disabled="(item.data as any).updating" :disabled="(item.data as ProjectListEntry).updating"
@click="updateProject(item.data)" @click="updateProject(item.data)"
> >
<DownloadIcon /> <DownloadIcon />
@@ -276,6 +276,7 @@ import {
RadialHeader, RadialHeader,
Toggle, Toggle,
} from '@modrinth/ui' } from '@modrinth/ui'
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import { computed, onUnmounted, ref, watch } 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 ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { getCurrentWebview } from '@tauri-apps/api/webview' import { getCurrentWebview } from '@tauri-apps/api/webview'
import dayjs from 'dayjs' 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({ const props = defineProps<{
instance: { instance: GameInstance
type: Object, options: InstanceType<typeof ContextMenu>
default() { offline: boolean
return {} playing: boolean
}, versions: Version[]
}, installed: boolean
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
},
},
})
type ProjectListEntryAuthor = { type ProjectListEntryAuthor = {
name: string name: string
@@ -356,13 +332,15 @@ type ProjectListEntry = {
author: ProjectListEntryAuthor | null author: ProjectListEntryAuthor | null
version: string | null version: string | null
file_name: string file_name: string
icon: string | null icon: string | undefined
disabled: boolean disabled: boolean
updateVersion?: string updateVersion?: string
outdated: boolean outdated: boolean
updated: dayjs.Dayjs updated: dayjs.Dayjs
project_type: string project_type: string
id?: string id?: string
updating?: boolean
selected?: boolean
} }
const isPackLocked = computed(() => { const isPackLocked = computed(() => {
@@ -375,17 +353,20 @@ const canUpdatePack = computed(() => {
const exportModal = ref(null) const exportModal = ref(null)
const projects = ref<ProjectListEntry[]>([]) const projects = ref<ProjectListEntry[]>([])
const selectedFiles = ref([]) const selectedFiles = ref<string[]>([])
const selectedProjects = computed(() => const selectedProjects = computed(() =>
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)), projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
) )
const selectionMap = ref(new Map()) const selectionMap = ref(new Map())
const initProjects = async (cacheBehaviour?) => { const initProjects = async (cacheBehaviour?: CacheBehaviour) => {
const newProjects: ProjectListEntry[] = [] 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 fetchProjects = []
const fetchVersions = [] const fetchVersions = []
@@ -397,21 +378,21 @@ const initProjects = async (cacheBehaviour?) => {
} }
const [modrinthProjects, modrinthVersions] = await Promise.all([ const [modrinthProjects, modrinthVersions] = await Promise.all([
await get_project_many(fetchProjects).catch(handleError), (await get_project_many(fetchProjects).catch(handleError)) as Project[],
await get_version_many(fetchVersions).catch(handleError), (await get_version_many(fetchVersions).catch(handleError)) as Version[],
]) ])
const [modrinthTeams, modrinthOrganizations] = await Promise.all([ const [modrinthTeams, modrinthOrganizations] = await Promise.all([
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError), (await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError)) as TeamMember[][],
await get_organization_many( (await get_organization_many(
modrinthProjects.map((x) => x.organization).filter((x) => !!x), modrinthProjects.map((x) => x.organization).filter((x) => !!x),
).catch(handleError), ).catch(handleError)) as Organization[],
]) ])
for (const [path, file] of Object.entries(profileProjects)) { for (const [path, file] of Object.entries(profileProjects)) {
if (file.metadata) { if (file.metadata) {
const project = modrinthProjects.find((x) => file.metadata.project_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) const version = modrinthVersions.find((x) => file.metadata?.version_id === x.id)
if (project && version) { if (project && version) {
const org = project.organization const org = project.organization
@@ -420,7 +401,7 @@ const initProjects = async (cacheBehaviour?) => {
const team = modrinthTeams.find((x) => x[0].team_id === project.team) const team = modrinthTeams.find((x) => x[0].team_id === project.team)
let author: ProjectListEntryAuthor | null let author: ProjectListEntryAuthor | null = null
if (org) { if (org) {
author = { author = {
name: org.name, name: org.name,
@@ -429,13 +410,13 @@ const initProjects = async (cacheBehaviour?) => {
} }
} else if (team) { } else if (team) {
const teamMember = team.find((x) => x.is_owner) const teamMember = team.find((x) => x.is_owner)
author = { if (teamMember) {
name: teamMember.user.username, author = {
slug: teamMember.user.username, name: teamMember.user.username,
type: 'user', slug: teamMember.user.username,
type: 'user',
}
} }
} else {
author = null
} }
newProjects.push({ newProjects.push({
@@ -464,7 +445,7 @@ const initProjects = async (cacheBehaviour?) => {
author: null, author: null,
version: null, version: null,
file_name: file.file_name, file_name: file.file_name,
icon: null, icon: undefined,
disabled: file.file_name.endsWith('.disabled'), disabled: file.file_name.endsWith('.disabled'),
outdated: false, outdated: false,
updated: dayjs(0), updated: dayjs(0),
@@ -488,7 +469,7 @@ const initProjects = async (cacheBehaviour?) => {
} }
await initProjects() await initProjects()
const modpackVersionModal = ref(null) const modpackVersionModal = ref<InstanceType<typeof ModpackVersionModal> | null>()
const installing = computed(() => props.instance.install_stage !== 'installed') const installing = computed(() => props.instance.install_stage !== 'installed')
const vintl = useVIntl() const vintl = useVIntl()
@@ -513,7 +494,7 @@ const messages = defineMessages({
const filterOptions: ComputedRef<FilterOption[]> = computed(() => { const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
const options: FilterOption[] = [] 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 map[item.project_type] = (map[item.project_type] || 0) + 1
return map return map
}, {}) }, {})
@@ -544,7 +525,7 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
return options return options
}) })
const selectedFilters = ref([]) const selectedFilters = ref<string[]>([])
const filteredProjects = computed(() => { const filteredProjects = computed(() => {
const updatesFilter = selectedFilters.value.includes('updates') const updatesFilter = selectedFilters.value.includes('updates')
const disabledFilter = selectedFilters.value.includes('disabled') 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)) { if (array.includes(value)) {
array.splice(array.indexOf(value), 1) array.splice(array.indexOf(value), 1)
} else { } else {
@@ -581,7 +562,7 @@ function toggleArray(array, value) {
const searchFilter = ref('') const searchFilter = ref('')
const selectAll = ref(false) const selectAll = ref(false)
const shareModal = ref(null) const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
const ascending = ref(true) const ascending = ref(true)
const sortColumn = ref('Name') const sortColumn = ref('Name')
const currentPage = ref(1) const currentPage = ref(1)
@@ -622,7 +603,7 @@ const search = computed(() => {
watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1)) watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1))
const sortProjects = (filter) => { const sortProjects = (filter: string) => {
if (sortColumn.value === filter) { if (sortColumn.value === filter) {
ascending.value = !ascending.value ascending.value = !ascending.value
} else { } 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)) { for (const [oldVal, newVal] of Object.entries(paths)) {
const index = projects.value.findIndex((x) => x.path === oldVal) const index = projects.value.findIndex((x) => x.path === oldVal)
@@ -649,7 +630,7 @@ const updateAll = async () => {
if (projects.value[index].updateVersion) { if (projects.value[index].updateVersion) {
projects.value[index].version = projects.value[index].updateVersion.version_number 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) { for (const project of setProjects) {
@@ -664,15 +645,15 @@ const updateAll = async () => {
}) })
} }
const updateProject = async (mod) => { const updateProject = async (mod: ProjectListEntry) => {
mod.updating = true mod.updating = true
await new Promise((resolve) => setTimeout(resolve, 0)) await new Promise((resolve) => setTimeout(resolve, 0))
mod.path = await update_project(props.instance.path, mod.path).catch(handleError) mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
mod.updating = false mod.updating = false
mod.outdated = false mod.outdated = false
mod.version = mod.updateVersion.version_number mod.version = mod.updateVersion?.version_number
mod.updateVersion = null mod.updateVersion = undefined
trackEvent('InstanceProjectUpdate', { trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader, 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. // 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] const lock = locks[mod.file_name]
while (lock) { while (lock) {
await new Promise((resolve) => { 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 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) await remove_project(props.instance.path, mod.path).catch(handleError)
projects.value = projects.value.filter((x) => mod.path !== x.path) projects.value = projects.value.filter((x) => mod.path !== x.path)
trackEvent('InstanceProjectRemove', { trackEvent('InstanceProjectRemove', {
loader: props.instance.loader, loader: props.instance.loader,
game_version: props.instance.game_version, game_version: props.instance.game_version,
id: mod.id, id: mod.data.id,
name: mod.name, name: mod.data.name,
project_type: mod.project_type, project_type: mod.data.project_type,
}) })
} }
const copyModLink = async (mod) => { const copyModLink = async (mod: ContentItem<ProjectListEntry>) => {
await navigator.clipboard.writeText( await navigator.clipboard.writeText(
`https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`, `https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`,
) )
@@ -744,15 +725,15 @@ const deleteSelected = async () => {
} }
const shareNames = 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 () => { 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 () => { const shareUrls = async () => {
await shareModal.value.show( await shareModal.value?.show(
functionValues.value functionValues.value
.filter((x) => x.slug) .filter((x) => x.slug)
.map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`) .map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`)
@@ -761,7 +742,7 @@ const shareUrls = async () => {
} }
const shareMarkdown = async () => { const shareMarkdown = async () => {
await shareModal.value.show( await shareModal.value?.show(
functionValues.value functionValues.value
.map((x) => { .map((x) => {
if (x.slug) { if (x.slug) {
@@ -826,15 +807,17 @@ const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
await initProjects() await initProjects()
}) })
const unlistenProfiles = await profile_listener(async (event) => { const unlistenProfiles = await profile_listener(
if ( async (event: { event: string; profile_path_id: string }) => {
event.profile_path_id === props.instance.path && if (
event.event === 'synced' && event.profile_path_id === props.instance.path &&
props.instance.install_stage !== 'pack_installing' event.event === 'synced' &&
) { props.instance.install_stage !== 'pack_installing'
await initProjects() ) {
} await initProjects()
}) }
},
)
onUnmounted(() => { onUnmounted(() => {
unlisten() unlisten()

View File

@@ -60,7 +60,7 @@
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </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"> <div class="flex flex-col w-full gap-2">
<WorldItem <WorldItem
v-for="world in filteredWorlds" v-for="world in filteredWorlds"
@@ -225,6 +225,11 @@ const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
await refreshAllWorlds() await refreshAllWorlds()
async function refreshServer(address: string) { async function refreshServer(address: string) {
if (!serverData.value[address]) {
serverData.value[address] = {
refreshing: true,
}
}
await refreshServerData(serverData.value[address], protocolVersion.value, address) await refreshServerData(serverData.value[address], protocolVersion.value, address)
} }
@@ -263,9 +268,12 @@ async function addServer(server: ServerWorld) {
async function editServer(server: ServerWorld) { async function editServer(server: ServerWorld) {
const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index) const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index)
if (index !== -1) { if (index !== -1) {
const oldServer = worlds.value[index] as ServerWorld
worlds.value[index] = server worlds.value[index] = server
sortWorlds(worlds.value) sortWorlds(worlds.value)
await refreshServer(server.address) if (oldServer.address !== server.address) {
await refreshServer(server.address)
}
} else { } else {
handleError(`Error refreshing server, refreshing all worlds`) handleError(`Error refreshing server, refreshing all worlds`)
await refreshAllWorlds() await refreshAllWorlds()
@@ -349,26 +357,34 @@ const supportsQuickPlay = computed(() =>
const filterOptions = computed(() => { const filterOptions = computed(() => {
const options: FilterBarOption[] = [] 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({ options.push({
id: 'singleplayer', id: 'singleplayer',
message: messages.singleplayer, message: messages.singleplayer,
}) })
}
if (worlds.value.some((x) => x.type === 'server')) {
options.push({ options.push({
id: 'server', id: 'server',
message: messages.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 ( if (
worlds.value.some( worlds.value.some(
(x) => (x) =>
x.type === 'server' && x.type === 'server' &&
!serverData.value[x.address]?.status && !serverData.value[x.address]?.status &&
!serverData.value[x.address]?.refreshing, !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({ options.push({

View File

@@ -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 { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { useRoute } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, shallowRef, watch } from 'vue' import { ref, shallowRef, watch } from 'vue'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
@@ -170,6 +170,7 @@ import { openUrl } from '@tauri-apps/plugin-opener'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const route = useRoute() const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
const themeStore = useTheming() const themeStore = useTheming()
@@ -247,6 +248,9 @@ async function install(version) {
installedVersion.value = version installedVersion.value = version
} }
}, },
(profile) => {
router.push(`/instance/${profile}`)
},
) )
} }

View File

@@ -23,8 +23,8 @@ export const useInstall = defineStore('installStore', {
setInstallConfirmModal(ref) { setInstallConfirmModal(ref) {
this.installConfirmModal = ref this.installConfirmModal = ref
}, },
showInstallConfirmModal(project, version_id, onInstall) { showInstallConfirmModal(project, version_id, onInstall, createInstanceCallback) {
this.installConfirmModal.show(project, version_id, onInstall) this.installConfirmModal.show(project, version_id, onInstall, createInstanceCallback)
}, },
setIncompatibilityWarningModal(ref) { setIncompatibilityWarningModal(ref) {
this.incompatibilityWarningModal = 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) const project = await get_project(projectId, 'must_revalidate').catch(handleError)
if (project.project_type === 'modpack') { if (project.project_type === 'modpack') {
@@ -49,7 +56,13 @@ export const install = async (projectId, versionId, instancePath, source, callba
const packs = await list().catch(handleError) const packs = await list().catch(handleError)
if (packs.length === 0 || !packs.find((pack) => pack.linked_data?.project_id === project.id)) { 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', { trackEvent('PackInstall', {
id: project.id, id: project.id,
@@ -61,7 +74,7 @@ export const install = async (projectId, versionId, instancePath, source, callba
callback(version) callback(version)
} else { } else {
const install = useInstall() const install = useInstall()
install.showInstallConfirmModal(project, version, callback) install.showInstallConfirmModal(project, version, callback, createInstanceCallback)
} }
} else { } else {
if (instancePath) { if (instancePath) {

View File

@@ -1,4 +1,4 @@
import { useTheming } from './theme' import { useTheming } from './theme.ts'
import { useBreadcrumbs } from './breadcrumbs' import { useBreadcrumbs } from './breadcrumbs'
import { useLoading } from './loading' import { useLoading } from './loading'
import { useNotifications, handleError } from './notifications' import { useNotifications, handleError } from './notifications'

View File

@@ -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`)
},
},
})

View 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
},
},
})

View File

@@ -9,5 +9,6 @@ edition = "2021"
theseus = { path = "../../packages/app-lib", features = ["cli"] } theseus = { path = "../../packages/app-lib", features = ["cli"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
webbrowser = "0.8.13" webbrowser = "0.8.13"
enumset = "1.1"
tracing = "0.1.37" tracing = "0.1.37"

View File

@@ -3,6 +3,7 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use enumset::EnumSet;
use theseus::prelude::*; use theseus::prelude::*;
use theseus::worlds::get_recent_worlds; use theseus::worlds::get_recent_worlds;
@@ -40,7 +41,7 @@ async fn main() -> theseus::Result<()> {
// Initialize state // Initialize state
State::init().await?; State::init().await?;
let worlds = get_recent_worlds(4).await?; let worlds = get_recent_worlds(4, EnumSet::all()).await?;
for world in worlds { for world in worlds {
println!( println!(
"World: {:?}/{:?} played at {:?}: {:#?}", "World: {:?}/{:?} played at {:?}: {:#?}",

View File

@@ -42,6 +42,7 @@ tracing-error = "0.2.0"
dashmap = "6.0.1" dashmap = "6.0.1"
paste = "1.0.15" paste = "1.0.15"
enumset = { version = "1.1", features = ["serde"] }
opener = { version = "0.7.2", features = ["reveal", "dbus-vendored"] } opener = { version = "0.7.2", features = ["reveal", "dbus-vendored"] }

View File

@@ -248,6 +248,7 @@ fn main() {
"get_recent_worlds", "get_recent_worlds",
"get_profile_worlds", "get_profile_worlds",
"get_singleplayer_world", "get_singleplayer_world",
"set_world_display_status",
"rename_world", "rename_world",
"reset_world_icon", "reset_world_icon",
"backup_world", "backup_world",

View File

@@ -1,10 +1,12 @@
use crate::api::Result; use crate::api::Result;
use either::Either; use either::Either;
use enumset::EnumSet;
use tauri::{AppHandle, Manager, Runtime}; use tauri::{AppHandle, Manager, Runtime};
use theseus::prelude::ProcessMetadata; use theseus::prelude::ProcessMetadata;
use theseus::profile::{get_full_path, QuickPlayType}; use theseus::profile::{get_full_path, QuickPlayType};
use theseus::worlds::{ use theseus::worlds::{
ServerPackStatus, ServerStatus, World, WorldWithProfile, DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
WorldWithProfile,
}; };
use theseus::{profile, worlds}; use theseus::{profile, worlds};
@@ -14,6 +16,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
get_recent_worlds, get_recent_worlds,
get_profile_worlds, get_profile_worlds,
get_singleplayer_world, get_singleplayer_world,
set_world_display_status,
rename_world, rename_world,
reset_world_icon, reset_world_icon,
backup_world, backup_world,
@@ -33,8 +36,13 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
pub async fn get_recent_worlds<R: Runtime>( pub async fn get_recent_worlds<R: Runtime>(
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
limit: usize, limit: usize,
display_statuses: Option<EnumSet<DisplayStatus>>,
) -> Result<Vec<WorldWithProfile>> { ) -> 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() { for world in result.iter_mut() {
adapt_world_icon(&app_handle, &mut world.world); adapt_world_icon(&app_handle, &mut world.world);
} }
@@ -59,8 +67,7 @@ pub async fn get_singleplayer_world<R: Runtime>(
instance: &str, instance: &str,
world: &str, world: &str,
) -> Result<World> { ) -> 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); adapt_world_icon(&app_handle, &mut world);
Ok(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] #[tauri::command]
pub async fn rename_world( pub async fn rename_world(
instance: &str, instance: &str,

View File

@@ -117,8 +117,7 @@ fn show_window(app: tauri::AppHandle) {
.set_type(MessageType::Error) .set_type(MessageType::Error)
.set_title("Initialization error") .set_title("Initialization error")
.set_text(&format!( .set_text(&format!(
"Cannot display application window due to an error:\n{}", "Cannot display application window due to an error:\n{e}"
e
)) ))
.show_alert() .show_alert()
.unwrap(); .unwrap();
@@ -138,8 +137,7 @@ fn is_dev() -> bool {
async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> { async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
window.set_decorations(b).map_err(|e| { window.set_decorations(b).map_err(|e| {
theseus::Error::from(theseus::ErrorKind::OtherError(format!( theseus::Error::from(theseus::ErrorKind::OtherError(format!(
"Failed to toggle decorations: {}", "Failed to toggle decorations: {e}"
e
))) )))
})?; })?;
Ok(()) Ok(())
@@ -320,7 +318,7 @@ fn main() {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution // 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", "Runtime(CreateWebview(WebView2Error(WindowsError",
) { ) {
MessageDialog::new() MessageDialog::new()
@@ -338,8 +336,7 @@ fn main() {
.set_type(MessageType::Error) .set_type(MessageType::Error)
.set_title("Initialization error") .set_title("Initialization error")
.set_text(&format!( .set_text(&format!(
"Cannot initialize application due to an error:\n{:?}", "Cannot initialize application due to an error:\n{e:?}"
e
)) ))
.show_alert() .show_alert()
.unwrap(); .unwrap();

View File

@@ -83,7 +83,7 @@
"csp": { "csp": {
"default-src": "'self' customprotocol: asset:", "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", "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:", "img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self'", "style-src": "'unsafe-inline' 'self'",
"script-src": "https://*.posthog.com 'self'", "script-src": "https://*.posthog.com 'self'",

View File

@@ -57,7 +57,7 @@ pub async fn fetch_forge(
ForgeVersion { ForgeVersion {
format_version, 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, raw: loader_version,
loader_version: version_split, loader_version: version_split,
game_version: game_version.clone(), game_version: game_version.clone(),
@@ -137,7 +137,7 @@ pub async fn fetch_neo(
Ok(ForgeVersion { Ok(ForgeVersion {
format_version: 2, 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, raw: loader_version,
loader_version: version_split, loader_version: version_split,
game_version: "1.20.1".to_string(), // All NeoForge Forge versions are for 1.20.1 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 { Ok(ForgeVersion {
format_version: 2, 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(), loader_version: loader_version.clone(),
raw: loader_version, raw: loader_version,
game_version, game_version,
@@ -502,7 +502,7 @@ async fn fetch(
)?; )?;
artifact.url = artifact.url =
format_url(&format!("maven/{}", artifact_path)); format_url(&format!("maven/{artifact_path}"));
return Ok(lib); return Ok(lib);
} }

View File

@@ -95,7 +95,7 @@ impl actix_web::ResponseError for OAuthError {
); );
if let Some(state) = self.state.as_ref() { if let Some(state) = self.state.as_ref() {
redirect_uri = format!("{}&state={}", redirect_uri, state); redirect_uri = format!("{redirect_uri}&state={state}");
} }
HttpResponse::Ok() HttpResponse::Ok()

View File

@@ -414,7 +414,7 @@ fn generate_access_token() -> String {
.take(60) .take(60)
.map(char::from) .map(char::from)
.collect::<String>(); .collect::<String>();
format!("mro_{}", random) format!("mro_{random}")
} }
async fn init_oauth_code_flow( async fn init_oauth_code_flow(

View File

@@ -32,7 +32,7 @@ impl Display for ErrorPage {
let html = include_str!("error.html") let html = include_str!("error.html")
.replace("{{ code }}", &self.code.to_string()) .replace("{{ code }}", &self.code.to_string())
.replace("{{ message }}", &self.message); .replace("{{ message }}", &self.message);
write!(f, "{}", html)?; write!(f, "{html}")?;
Ok(()) Ok(())
} }

View File

@@ -103,8 +103,7 @@ impl MinecraftGameVersion {
} }
_ => { _ => {
return Err(DatabaseError::SchemaError(format!( return Err(DatabaseError::SchemaError(format!(
"Game version requires field value to be an enum: {:?}", "Game version requires field value to be an enum: {version_field:?}"
version_field
))); )));
} }
}; };

View File

@@ -1080,8 +1080,7 @@ impl VersionFieldValue {
let field_name = field_type.to_str(); let field_name = field_type.to_str();
let did_not_exist_error = |field_name: &str, desired_field: &str| { let did_not_exist_error = |field_name: &str, desired_field: &str| {
DatabaseError::SchemaError(format!( DatabaseError::SchemaError(format!(
"Field name {} for field {} in does not exist", "Field name {desired_field} for field {field_name} in does not exist"
desired_field, field_name
)) ))
}; };
@@ -1103,8 +1102,7 @@ impl VersionFieldValue {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if field_id.len() > 1 { if field_id.len() > 1 {
return Err(DatabaseError::SchemaError(format!( return Err(DatabaseError::SchemaError(format!(
"Multiple field ids for field {}", "Multiple field ids for field {field_name}"
field_name
))); )));
} }

View File

@@ -912,7 +912,7 @@ impl Version {
file.hashes.iter().map(|(algo, hash)| { file.hashes.iter().map(|(algo, hash)| {
( (
VERSION_FILES_NAMESPACE, VERSION_FILES_NAMESPACE,
Some(format!("{}_{}", algo, hash)), Some(format!("{algo}_{hash}")),
) )
}) })
}, },

View File

@@ -80,10 +80,9 @@ impl From<DBNotification> for Notification {
} => ( } => (
"A project you follow has been updated!".to_string(), "A project you follow has been updated!".to_string(),
format!( format!(
"The project {} has released a new version: {}", "The project {project_id} has released a new version: {version_id}"
project_id, version_id
), ),
format!("/project/{}/version/{}", project_id, version_id), format!("/project/{project_id}/version/{version_id}"),
vec![], vec![],
), ),
NotificationBody::TeamInvite { NotificationBody::TeamInvite {
@@ -93,8 +92,8 @@ impl From<DBNotification> for Notification {
.. ..
} => ( } => (
"You have been invited to join a team!".to_string(), "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!("An invite has been sent for you to be {role} of a team"),
format!("/project/{}", project_id), format!("/project/{project_id}"),
vec![ vec![
NotificationAction { NotificationAction {
name: "Accept".to_string(), name: "Accept".to_string(),
@@ -117,10 +116,9 @@ impl From<DBNotification> for Notification {
} => ( } => (
"You have been invited to join an organization!".to_string(), "You have been invited to join an organization!".to_string(),
format!( format!(
"An invite has been sent for you to be {} of an organization", "An invite has been sent for you to be {role} of an organization"
role
), ),
format!("/organization/{}", organization_id), format!("/organization/{organization_id}"),
vec![ vec![
NotificationAction { NotificationAction {
name: "Accept".to_string(), name: "Accept".to_string(),
@@ -149,7 +147,7 @@ impl From<DBNotification> for Notification {
old_status.as_friendly_str(), old_status.as_friendly_str(),
new_status.as_friendly_str() new_status.as_friendly_str()
), ),
format!("/project/{}", project_id), format!("/project/{project_id}"),
vec![], vec![],
), ),
NotificationBody::ModeratorMessage { NotificationBody::ModeratorMessage {
@@ -160,9 +158,9 @@ impl From<DBNotification> for Notification {
"A moderator has sent you a message!".to_string(), "A moderator has sent you a message!".to_string(),
"Click on the link to read more.".to_string(), "Click on the link to read more.".to_string(),
if let Some(project_id) = project_id { if let Some(project_id) = project_id {
format!("/project/{}", project_id) format!("/project/{project_id}")
} else if let Some(report_id) = report_id { } else if let Some(report_id) = report_id {
format!("/project/{}", report_id) format!("/project/{report_id}")
} else { } else {
"#".to_string() "#".to_string()
}, },

View File

@@ -222,8 +222,7 @@ pub async fn delphi_result_ingest(
for (issue, trace) in &body.issues { for (issue, trace) in &body.issues {
for (path, code) in trace { for (path, code) in trace {
header.push_str(&format!( header.push_str(&format!(
"\n issue {issue} found at file {}: \n ```\n{}\n```", "\n issue {issue} found at file {path}: \n ```\n{code}\n```"
path, code
)); ));
} }
} }
@@ -242,10 +241,8 @@ pub async fn delphi_result_ingest(
for (issue, trace) in &body.issues { for (issue, trace) in &body.issues {
for path in trace.keys() { for path in trace.keys() {
thread_header.push_str(&format!( thread_header
"\n\n- issue {issue} found at file {}", .push_str(&format!("\n\n- issue {issue} found at file {path}"));
path
));
} }
if trace.is_empty() { if trace.is_empty() {

View File

@@ -247,7 +247,7 @@ impl AuthProvider {
state: String, state: String,
) -> Result<String, AuthenticationError> { ) -> Result<String, AuthenticationError> {
let self_addr = dotenvy::var("SELF_ADDR")?; 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); let redirect_uri = urlencoding::encode(&raw_redirect_uri);
Ok(match self { Ok(match self {
@@ -255,30 +255,24 @@ impl AuthProvider {
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?; let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
format!( format!(
"https://github.com/login/oauth/authorize?client_id={}&prompt=select_account&state={}&scope=read%3Auser%20user%3Aemail&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}",
client_id,
state,
redirect_uri,
) )
} }
AuthProvider::Discord => { AuthProvider::Discord => {
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?; 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 => { AuthProvider::Microsoft => {
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?; 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 => { AuthProvider::GitLab => {
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?; let client_id = dotenvy::var("GITLAB_CLIENT_ID")?;
format!( format!(
"https://gitlab.com/oauth/authorize?client_id={}&state={}&scope=read_user+profile+email&response_type=code&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}",
client_id,
state,
redirect_uri,
) )
} }
AuthProvider::Google => { AuthProvider::Google => {
@@ -342,8 +336,7 @@ impl AuthProvider {
let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?; let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?;
let url = format!( let url = format!(
"https://github.com/login/oauth/access_token?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}"
client_id, client_secret, code, redirect_uri
); );
let token: AccessToken = reqwest::Client::new() let token: AccessToken = reqwest::Client::new()
@@ -482,9 +475,8 @@ impl AuthProvider {
form.insert("openid.mode".to_string(), "check_authentication"); form.insert("openid.mode".to_string(), "check_authentication");
for val in signed.split(',') { for val in signed.split(',') {
if let Some(arr_val) = query.get(&format!("openid.{}", val)) if let Some(arr_val) = query.get(&format!("openid.{val}")) {
{ form.insert(format!("openid.{val}"), &**arr_val);
form.insert(format!("openid.{}", val), &**arr_val);
} }
} }
@@ -621,8 +613,7 @@ impl AuthProvider {
email: discord_user.email, email: discord_user.email,
avatar_url: discord_user.avatar.map(|x| { avatar_url: discord_user.avatar.map(|x| {
format!( format!(
"https://cdn.discordapp.com/avatars/{}/{}.webp", "https://cdn.discordapp.com/avatars/{id}/{x}.webp"
id, x
) )
}), }),
bio: None, bio: None,
@@ -741,9 +732,7 @@ impl AuthProvider {
let response: String = reqwest::get( let response: String = reqwest::get(
&format!( &format!(
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={}&steamids={}", "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={api_key}&steamids={token}"
api_key,
token
) )
) )
.await? .await?
@@ -1367,7 +1356,7 @@ pub async fn create_account_with_password(
if let Some(feedback) = if let Some(feedback) =
score.feedback().clone().and_then(|x| x.warning()) score.feedback().clone().and_then(|x| x.warning())
{ {
format!("Password too weak: {}", feedback) format!("Password too weak: {feedback}")
} else { } else {
"Specified password is too weak! Please improve its strength." "Specified password is too weak! Please improve its strength."
.to_string() .to_string()
@@ -2030,7 +2019,7 @@ pub async fn change_password(
if let Some(feedback) = if let Some(feedback) =
score.feedback().clone().and_then(|x| x.warning()) score.feedback().clone().and_then(|x| x.warning())
{ {
format!("Password too weak: {}", feedback) format!("Password too weak: {feedback}")
} else { } else {
"Specified password is too weak! Please improve its strength.".to_string() "Specified password is too weak! Please improve its strength.".to_string()
}, },
@@ -2085,8 +2074,8 @@ pub async fn change_password(
send_email( send_email(
email, email,
&format!("Password {}", changed), &format!("Password {changed}"),
&format!("Your password has been {} on your account.", 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).", "If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
None, None,
)?; )?;

View File

@@ -113,7 +113,7 @@ pub async fn create_pat(
.take(60) .take(60)
.map(char::from) .map(char::from)
.collect::<String>(); .collect::<String>();
let token = format!("mrp_{}", token); let token = format!("mrp_{token}");
let name = info.name.clone(); let name = info.name.clone();
database::models::pat_item::PersonalAccessToken { database::models::pat_item::PersonalAccessToken {

View File

@@ -116,7 +116,7 @@ pub async fn forge_updates(
for game_version in &game_versions { for game_version in &game_versions {
response response
.promos .promos
.entry(format!("{}-recommended", game_version)) .entry(format!("{game_version}-recommended"))
.or_insert_with(|| version.version_number.clone()); .or_insert_with(|| version.version_number.clone());
} }
} }
@@ -124,7 +124,7 @@ pub async fn forge_updates(
for game_version in &game_versions { for game_version in &game_versions {
response response
.promos .promos
.entry(format!("{}-latest", game_version)) .entry(format!("{game_version}-latest"))
.or_insert_with(|| version.version_number.clone()); .or_insert_with(|| version.version_number.clone());
} }
} }

View File

@@ -144,7 +144,7 @@ where
match ( match (
"Content-Type", "Content-Type",
format!("multipart/form-data; boundary={}", boundary).as_str(), format!("multipart/form-data; boundary={boundary}").as_str(),
) )
.try_into_pair() .try_into_pair()
{ {
@@ -153,8 +153,7 @@ where
} }
Err(err) => { Err(err) => {
CreateError::InvalidInput(format!( CreateError::InvalidInput(format!(
"Error inserting test header: {:?}.", "Error inserting test header: {err:?}."
err
)); ));
} }
}; };

View File

@@ -424,7 +424,7 @@ pub async fn collection_icon_edit(
let collection_id: CollectionId = collection_item.id.into(); let collection_id: CollectionId = collection_item.id.into();
let upload_result = crate::util::img::upload_image_optimized( let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{}", collection_id), &format!("data/{collection_id}"),
bytes.freeze(), bytes.freeze(),
&ext.ext, &ext.ext,
Some(96), Some(96),

View File

@@ -393,7 +393,7 @@ pub async fn oauth_client_icon_edit(
) )
.await?; .await?;
let upload_result = upload_image_optimized( let upload_result = upload_image_optimized(
&format!("data/{}", client_id), &format!("data/{client_id}"),
bytes.freeze(), bytes.freeze(),
&ext.ext, &ext.ext,
Some(96), Some(96),

View File

@@ -1096,7 +1096,7 @@ pub async fn organization_icon_edit(
let organization_id: OrganizationId = organization_item.id.into(); let organization_id: OrganizationId = organization_item.id.into();
let upload_result = crate::util::img::upload_image_optimized( let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{}", organization_id), &format!("data/{organization_id}"),
bytes.freeze(), bytes.freeze(),
&ext.ext, &ext.ext,
Some(96), Some(96),

View File

@@ -676,8 +676,7 @@ pub async fn cancel_payout(
.make_paypal_request::<(), ()>( .make_paypal_request::<(), ()>(
Method::POST, Method::POST,
&format!( &format!(
"payments/payouts-item/{}/cancel", "payments/payouts-item/{platform_id}/cancel"
platform_id
), ),
None, None,
None, None,
@@ -689,7 +688,7 @@ pub async fn cancel_payout(
payouts payouts
.make_tremendous_request::<(), ()>( .make_tremendous_request::<(), ()>(
Method::POST, Method::POST,
&format!("rewards/{}/cancel", platform_id), &format!("rewards/{platform_id}/cancel"),
None, None,
) )
.await?; .await?;

View File

@@ -810,8 +810,7 @@ async fn project_create_inner(
|| image.context.inner_id().is_some() || image.context.inner_id().is_some()
{ {
return Err(CreateError::InvalidInput(format!( return Err(CreateError::InvalidInput(format!(
"Image {} is not unused and in the 'project' context", "Image {image_id} is not unused and in the 'project' context"
image_id
))); )));
} }
@@ -830,8 +829,7 @@ async fn project_create_inner(
image_item::Image::clear_cache(image.id.into(), redis).await?; image_item::Image::clear_cache(image.id.into(), redis).await?;
} else { } else {
return Err(CreateError::InvalidInput(format!( return Err(CreateError::InvalidInput(format!(
"Image {} does not exist", "Image {image_id} does not exist"
image_id
))); )));
} }
} }

View File

@@ -710,10 +710,8 @@ pub async fn project_edit(
)); ));
} }
let ids_to_delete = links let ids_to_delete =
.iter() links.keys().cloned().collect::<Vec<String>>();
.map(|(name, _)| name.clone())
.collect::<Vec<String>>();
// Deletes all links from hashmap- either will be deleted or be replaced // Deletes all links from hashmap- either will be deleted or be replaced
sqlx::query!( sqlx::query!(
" "
@@ -1270,10 +1268,7 @@ pub async fn projects_edit(
.await?; .await?;
if let Some(links) = &bulk_edit_project.link_urls { if let Some(links) = &bulk_edit_project.link_urls {
let ids_to_delete = links let ids_to_delete = links.keys().cloned().collect::<Vec<String>>();
.iter()
.map(|(name, _)| name.clone())
.collect::<Vec<String>>();
// Deletes all links from hashmap- either will be deleted or be replaced // Deletes all links from hashmap- either will be deleted or be replaced
sqlx::query!( sqlx::query!(
" "
@@ -1482,7 +1477,7 @@ pub async fn project_icon_edit(
let project_id: ProjectId = project_item.inner.id.into(); let project_id: ProjectId = project_item.inner.id.into();
let upload_result = upload_image_optimized( let upload_result = upload_image_optimized(
&format!("data/{}", project_id), &format!("data/{project_id}"),
bytes.freeze(), bytes.freeze(),
&ext.ext, &ext.ext,
Some(96), Some(96),
@@ -1700,7 +1695,7 @@ pub async fn add_gallery_item(
let id: ProjectId = project_item.inner.id.into(); let id: ProjectId = project_item.inner.id.into();
let upload_result = upload_image_optimized( let upload_result = upload_image_optimized(
&format!("data/{}/images", id), &format!("data/{id}/images"),
bytes.freeze(), bytes.freeze(),
&ext.ext, &ext.ext,
Some(350), Some(350),

View File

@@ -178,8 +178,7 @@ pub async fn report_create(
|| image.context.inner_id().is_some() || image.context.inner_id().is_some()
{ {
return Err(ApiError::InvalidInput(format!( return Err(ApiError::InvalidInput(format!(
"Image {} is not unused and in the 'report' context", "Image {image_id} is not unused and in the 'report' context"
image_id
))); )));
} }
@@ -198,8 +197,7 @@ pub async fn report_create(
image_item::Image::clear_cache(image.id.into(), &redis).await?; image_item::Image::clear_cache(image.id.into(), &redis).await?;
} else { } else {
return Err(ApiError::InvalidInput(format!( return Err(ApiError::InvalidInput(format!(
"Image {} could not be found", "Image {image_id} could not be found"
image_id
))); )));
} }
} }

View File

@@ -527,8 +527,7 @@ pub async fn thread_send_message(
) || image.context.inner_id().is_some() ) || image.context.inner_id().is_some()
{ {
return Err(ApiError::InvalidInput(format!( return Err(ApiError::InvalidInput(format!(
"Image {} is not unused and in the 'thread_message' context", "Image {image_id} is not unused and in the 'thread_message' context"
image_id
))); )));
} }
@@ -548,8 +547,7 @@ pub async fn thread_send_message(
.await?; .await?;
} else { } else {
return Err(ApiError::InvalidInput(format!( return Err(ApiError::InvalidInput(format!(
"Image {} does not exist", "Image {image_id} does not exist"
image_id
))); )));
} }
} }

View File

@@ -595,7 +595,7 @@ pub async fn user_icon_edit(
let user_id: UserId = actual_user.id.into(); let user_id: UserId = actual_user.id.into();
let upload_result = crate::util::img::upload_image_optimized( let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{}", user_id), &format!("data/{user_id}"),
bytes.freeze(), bytes.freeze(),
&ext.ext, &ext.ext,
Some(96), Some(96),

View File

@@ -486,8 +486,7 @@ async fn version_create_inner(
|| image.context.inner_id().is_some() || image.context.inner_id().is_some()
{ {
return Err(CreateError::InvalidInput(format!( return Err(CreateError::InvalidInput(format!(
"Image {} is not unused and in the 'version' context", "Image {image_id} is not unused and in the 'version' context"
image_id
))); )));
} }
@@ -506,8 +505,7 @@ async fn version_create_inner(
image_item::Image::clear_cache(image.id.into(), redis).await?; image_item::Image::clear_cache(image.id.into(), redis).await?;
} else { } else {
return Err(CreateError::InvalidInput(format!( return Err(CreateError::InvalidInput(format!(
"Image {} does not exist", "Image {image_id} does not exist"
image_id
))); )));
} }
} }
@@ -810,7 +808,7 @@ pub async fn upload_file(
) -> Result<(), CreateError> { ) -> Result<(), CreateError> {
let (file_name, file_extension) = get_name_ext(content_disposition)?; 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( return Err(CreateError::InvalidInput(
"Duplicate files are not allowed to be uploaded to Modrinth!" "Duplicate files are not allowed to be uploaded to Modrinth!"
.to_string(), .to_string(),

View File

@@ -67,5 +67,5 @@ pub async fn push_back_user_expiry(
} }
fn get_field_name(user: UserId) -> String { fn get_field_name(user: UserId) -> String {
format!("user_status:{}", user) format!("user_status:{user}")
} }

View File

@@ -34,7 +34,7 @@ impl AppendsMultipart for TestRequest {
let (boundary, payload) = generate_multipart(data); let (boundary, payload) = generate_multipart(data);
self.append_header(( self.append_header((
"Content-Type", "Content-Type",
format!("multipart/form-data; boundary={}", boundary), format!("multipart/form-data; boundary={boundary}"),
)) ))
.set_payload(payload) .set_payload(payload)
} }
@@ -62,17 +62,12 @@ pub fn generate_multipart(
if let Some(filename) = &segment.filename { if let Some(filename) = &segment.filename {
payload.extend_from_slice( payload.extend_from_slice(
format!("; filename=\"{filename}\"", filename = filename) format!("; filename=\"{filename}\"").as_bytes(),
.as_bytes(),
); );
} }
if let Some(content_type) = &segment.content_type { if let Some(content_type) = &segment.content_type {
payload.extend_from_slice( payload.extend_from_slice(
format!( format!("\r\nContent-Type: {content_type}").as_bytes(),
"\r\nContent-Type: {content_type}",
content_type = content_type
)
.as_bytes(),
); );
} }
payload.extend_from_slice(b"\r\n\r\n"); 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(b"\r\n");
} }
payload.extend_from_slice( payload.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
format!("--{boundary}--\r\n", boundary = boundary).as_bytes(),
);
(boundary, Bytes::from(payload)) (boundary, Bytes::from(payload))
} }

View File

@@ -51,8 +51,7 @@ pub async fn upload_image_optimized(
let content_type = crate::util::ext::get_image_content_type(file_extension) let content_type = crate::util::ext::get_image_content_type(file_extension)
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInput(format!( ApiError::InvalidInput(format!(
"Invalid format for image: {}", "Invalid format for image: {file_extension}"
file_extension
)) ))
})?; })?;
@@ -91,7 +90,7 @@ pub async fn upload_image_optimized(
let upload_data = file_host let upload_data = file_host
.upload_file( .upload_file(
content_type, content_type,
&format!("{}/{}.{}", upload_folder, hash, file_extension), &format!("{upload_folder}/{hash}.{file_extension}"),
bytes, bytes,
) )
.await?; .await?;

View File

@@ -71,7 +71,7 @@ impl ApiV2 {
}; };
let req = test::TestRequest::get() let req = test::TestRequest::get()
.uri(&format!("/v2/search?{}{}", query_field, facets_field)) .uri(&format!("/v2/search?{query_field}{facets_field}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
let resp = self.call(req).await; let resp = self.call(req).await;
@@ -99,7 +99,7 @@ impl ApiProject for ApiV2 {
// Approve as a moderator. // Approve as a moderator.
let req = TestRequest::patch() let req = TestRequest::patch()
.uri(&format!("/v2/project/{}", slug)) .uri(&format!("/v2/project/{slug}"))
.append_pat(MOD_USER_PAT) .append_pat(MOD_USER_PAT)
.set_json(json!( .set_json(json!(
{ {
@@ -114,7 +114,7 @@ impl ApiProject for ApiV2 {
// Get project's versions // Get project's versions
let req = TestRequest::get() let req = TestRequest::get()
.uri(&format!("/v2/project/{}/version", slug)) .uri(&format!("/v2/project/{slug}/version"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
let resp = self.call(req).await; let resp = self.call(req).await;
@@ -217,7 +217,7 @@ impl ApiProject for ApiV2 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::get() 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) .append_pat(pat)
.to_request(); .to_request();
self.call(req).await self.call(req).await
@@ -260,7 +260,7 @@ impl ApiProject for ApiV2 {
) -> ServiceResponse { ) -> ServiceResponse {
let projects_str = ids_or_slugs let projects_str = ids_or_slugs
.iter() .iter()
.map(|s| format!("\"{}\"", s)) .map(|s| format!("\"{s}\""))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
@@ -490,13 +490,13 @@ impl ApiProject for ApiV2 {
featured = featured featured = featured
); );
if let Some(title) = title { if let Some(title) = title {
url.push_str(&format!("&title={}", title)); url.push_str(&format!("&title={title}"));
} }
if let Some(description) = description { if let Some(description) = description {
url.push_str(&format!("&description={}", description)); url.push_str(&format!("&description={description}"));
} }
if let Some(ordering) = ordering { if let Some(ordering) = ordering {
url.push_str(&format!("&ordering={}", ordering)); url.push_str(&format!("&ordering={ordering}"));
} }
let req = test::TestRequest::post() let req = test::TestRequest::post()
@@ -542,10 +542,7 @@ impl ApiProject for ApiV2 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::delete() let req = test::TestRequest::delete()
.uri(&format!( .uri(&format!("/v2/project/{id_or_slug}/gallery?url={url}"))
"/v2/project/{id_or_slug}/gallery?url={url}",
url = url
))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();

View File

@@ -11,7 +11,7 @@ impl ApiUser for ApiV2 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::get() let req = test::TestRequest::get()
.uri(&format!("/v2/user/{}", user_id_or_username)) .uri(&format!("/v2/user/{user_id_or_username}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
self.call(req).await self.call(req).await
@@ -32,7 +32,7 @@ impl ApiUser for ApiV2 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/v2/user/{}", user_id_or_username)) .uri(&format!("/v2/user/{user_id_or_username}"))
.append_pat(pat) .append_pat(pat)
.set_json(patch) .set_json(patch)
.to_request(); .to_request();
@@ -46,7 +46,7 @@ impl ApiUser for ApiV2 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::delete() let req = test::TestRequest::delete()
.uri(&format!("/v2/user/{}", user_id_or_username)) .uri(&format!("/v2/user/{user_id_or_username}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();

View File

@@ -399,18 +399,18 @@ impl ApiVersion for ApiV2 {
)); ));
} }
if let Some(featured) = featured { 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 { 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 { if let Some(limit) = limit {
let limit = limit.to_string(); let limit = limit.to_string();
query_string.push_str(&format!("&limit={}", limit)); query_string.push_str(&format!("&limit={limit}"));
} }
if let Some(offset) = offset { if let Some(offset) = offset {
let offset = offset.to_string(); let offset = offset.to_string();
query_string.push_str(&format!("&offset={}", offset)); query_string.push_str(&format!("&offset={offset}"));
} }
let req = test::TestRequest::get() let req = test::TestRequest::get()
@@ -480,7 +480,7 @@ impl ApiVersion for ApiV2 {
) -> ServiceResponse { ) -> ServiceResponse {
let ids = url_encode_json_serialized_vec(&version_ids); let ids = url_encode_json_serialized_vec(&version_ids);
let request = test::TestRequest::get() let request = test::TestRequest::get()
.uri(&format!("/v2/versions?ids={}", ids)) .uri(&format!("/v2/versions?ids={ids}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
self.call(request).await self.call(request).await

View File

@@ -157,7 +157,7 @@ impl ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::get() 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) .append_pat(pat)
.to_request(); .to_request();
self.call(req).await self.call(req).await

View File

@@ -104,8 +104,7 @@ impl ApiV3 {
code: auth_code, code: auth_code,
redirect_uri: original_redirect_uri, redirect_uri: original_redirect_uri,
client_id: serde_json::from_str(&format!( client_id: serde_json::from_str(&format!(
"\"{}\"", "\"{client_id}\""
client_id
)) ))
.unwrap(), .unwrap(),
}) })

View File

@@ -47,7 +47,7 @@ impl ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> Vec<OAuthClient> { ) -> Vec<OAuthClient> {
let req = TestRequest::get() let req = TestRequest::get()
.uri(&format!("/v3/user/{}/oauth_apps", user_id)) .uri(&format!("/v3/user/{user_id}/oauth_apps"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
let resp = self.call(req).await; let resp = self.call(req).await;
@@ -62,7 +62,7 @@ impl ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = TestRequest::get() let req = TestRequest::get()
.uri(&format!("/_internal/oauth/app/{}", client_id)) .uri(&format!("/_internal/oauth/app/{client_id}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
@@ -93,7 +93,7 @@ impl ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = TestRequest::delete() let req = TestRequest::delete()
.uri(&format!("/_internal/oauth/app/{}", client_id)) .uri(&format!("/_internal/oauth/app/{client_id}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();

View File

@@ -53,7 +53,7 @@ impl ApiProject for ApiV3 {
// Approve as a moderator. // Approve as a moderator.
let req = TestRequest::patch() let req = TestRequest::patch()
.uri(&format!("/v3/project/{}", slug)) .uri(&format!("/v3/project/{slug}"))
.append_pat(MOD_USER_PAT) .append_pat(MOD_USER_PAT)
.set_json(json!( .set_json(json!(
{ {
@@ -69,7 +69,7 @@ impl ApiProject for ApiV3 {
// Get project's versions // Get project's versions
let req = TestRequest::get() let req = TestRequest::get()
.uri(&format!("/v3/project/{}/version", slug)) .uri(&format!("/v3/project/{slug}/version"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
let resp = self.call(req).await; let resp = self.call(req).await;
@@ -172,7 +172,7 @@ impl ApiProject for ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::get() 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) .append_pat(pat)
.to_request(); .to_request();
self.call(req).await self.call(req).await
@@ -215,7 +215,7 @@ impl ApiProject for ApiV3 {
) -> ServiceResponse { ) -> ServiceResponse {
let projects_str = ids_or_slugs let projects_str = ids_or_slugs
.iter() .iter()
.map(|s| format!("\"{}\"", s)) .map(|s| format!("\"{s}\""))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
@@ -363,13 +363,13 @@ impl ApiProject for ApiV3 {
featured = featured featured = featured
); );
if let Some(title) = title { if let Some(title) = title {
url.push_str(&format!("&title={}", title)); url.push_str(&format!("&title={title}"));
} }
if let Some(description) = description { if let Some(description) = description {
url.push_str(&format!("&description={}", description)); url.push_str(&format!("&description={description}"));
} }
if let Some(ordering) = ordering { if let Some(ordering) = ordering {
url.push_str(&format!("&ordering={}", ordering)); url.push_str(&format!("&ordering={ordering}"));
} }
let req = test::TestRequest::post() let req = test::TestRequest::post()
@@ -416,10 +416,7 @@ impl ApiProject for ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::delete() let req = test::TestRequest::delete()
.uri(&format!( .uri(&format!("/v3/project/{id_or_slug}/gallery?url={url}"))
"/v3/project/{id_or_slug}/gallery?url={url}",
url = url
))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
@@ -562,7 +559,7 @@ impl ApiV3 {
}; };
let req = test::TestRequest::get() let req = test::TestRequest::get()
.uri(&format!("/v3/search?{}{}", query_field, facets_field)) .uri(&format!("/v3/search?{query_field}{facets_field}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
let resp = self.call(req).await; let resp = self.call(req).await;
@@ -583,12 +580,12 @@ impl ApiV3 {
let version_string: String = let version_string: String =
serde_json::to_string(&id_or_slugs).unwrap(); serde_json::to_string(&id_or_slugs).unwrap();
let version_string = urlencoding::encode(&version_string); let version_string = urlencoding::encode(&version_string);
format!("version_ids={}", version_string) format!("version_ids={version_string}")
} else { } else {
let projects_string: String = let projects_string: String =
serde_json::to_string(&id_or_slugs).unwrap(); serde_json::to_string(&id_or_slugs).unwrap();
let projects_string = urlencoding::encode(&projects_string); let projects_string = urlencoding::encode(&projects_string);
format!("project_ids={}", projects_string) format!("project_ids={projects_string}")
}; };
let mut extra_args = String::new(); let mut extra_args = String::new();
@@ -605,10 +602,8 @@ impl ApiV3 {
extra_args.push_str(&format!("&end_date={end_date}")); extra_args.push_str(&format!("&end_date={end_date}"));
} }
if let Some(resolution_minutes) = resolution_minutes { if let Some(resolution_minutes) = resolution_minutes {
extra_args.push_str(&format!( extra_args
"&resolution_minutes={}", .push_str(&format!("&resolution_minutes={resolution_minutes}"));
resolution_minutes
));
} }
let req = test::TestRequest::get() let req = test::TestRequest::get()

View File

@@ -76,7 +76,7 @@ impl ApiV3 {
loader_field: &str, loader_field: &str,
) -> ServiceResponse { ) -> ServiceResponse {
let req = TestRequest::get() 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) .append_pat(ADMIN_USER_PAT)
.to_request(); .to_request();
self.call(req).await self.call(req).await

View File

@@ -13,7 +13,7 @@ impl ApiUser for ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::get() let req = test::TestRequest::get()
.uri(&format!("/v3/user/{}", user_id_or_username)) .uri(&format!("/v3/user/{user_id_or_username}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
self.call(req).await self.call(req).await
@@ -34,7 +34,7 @@ impl ApiUser for ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/v3/user/{}", user_id_or_username)) .uri(&format!("/v3/user/{user_id_or_username}"))
.append_pat(pat) .append_pat(pat)
.set_json(patch) .set_json(patch)
.to_request(); .to_request();
@@ -48,7 +48,7 @@ impl ApiUser for ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::delete() let req = test::TestRequest::delete()
.uri(&format!("/v3/user/{}", user_id_or_username)) .uri(&format!("/v3/user/{user_id_or_username}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
self.call(req).await self.call(req).await

View File

@@ -432,18 +432,18 @@ impl ApiVersion for ApiV3 {
)); ));
} }
if let Some(featured) = featured { 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 { 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 { if let Some(limit) = limit {
let limit = limit.to_string(); let limit = limit.to_string();
query_string.push_str(&format!("&limit={}", limit)); query_string.push_str(&format!("&limit={limit}"));
} }
if let Some(offset) = offset { if let Some(offset) = offset {
let offset = offset.to_string(); let offset = offset.to_string();
query_string.push_str(&format!("&offset={}", offset)); query_string.push_str(&format!("&offset={offset}"));
} }
let req = test::TestRequest::get() let req = test::TestRequest::get()
@@ -513,7 +513,7 @@ impl ApiVersion for ApiV3 {
) -> ServiceResponse { ) -> ServiceResponse {
let ids = url_encode_json_serialized_vec(&version_ids); let ids = url_encode_json_serialized_vec(&version_ids);
let request = test::TestRequest::get() let request = test::TestRequest::get()
.uri(&format!("/v3/versions?ids={}", ids)) .uri(&format!("/v3/versions?ids={ids}"))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
self.call(request).await self.call(request).await
@@ -546,10 +546,7 @@ impl ApiVersion for ApiV3 {
Some(file), Some(file),
); );
let request = test::TestRequest::post() let request = test::TestRequest::post()
.uri(&format!( .uri(&format!("/v3/version/{version_id}/file"))
"/v3/version/{version_id}/file",
version_id = version_id
))
.append_pat(pat) .append_pat(pat)
.set_multipart(m) .set_multipart(m)
.to_request(); .to_request();
@@ -562,10 +559,7 @@ impl ApiVersion for ApiV3 {
pat: Option<&str>, pat: Option<&str>,
) -> ServiceResponse { ) -> ServiceResponse {
let request = test::TestRequest::delete() let request = test::TestRequest::delete()
.uri(&format!( .uri(&format!("/v3/version/{version_id}"))
"/v3/version/{version_id}",
version_id = version_id
))
.append_pat(pat) .append_pat(pat)
.to_request(); .to_request();
self.call(request).await self.call(request).await

View File

@@ -137,7 +137,7 @@ impl TemporaryDatabase {
dotenvy::var("DATABASE_URL").expect("No database URL"); dotenvy::var("DATABASE_URL").expect("No database URL");
let mut template_url = let mut template_url =
Url::parse(&url).expect("Invalid database 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()) let pool = PgPool::connect(template_url.as_str())
.await .await

View File

@@ -470,8 +470,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != ProjectPermissions::empty() { if p != ProjectPermissions::empty() {
return Err(format!( return Err(format!(
"Test 1 failed. Expected no permissions, got {:?}", "Test 1 failed. Expected no permissions, got {p:?}"
p
)); ));
} }
@@ -511,8 +510,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != ProjectPermissions::empty() { if p != ProjectPermissions::empty() {
return Err(format!( return Err(format!(
"Test 2 failed. Expected no permissions, got {:?}", "Test 2 failed. Expected no permissions, got {p:?}"
p
)); ));
} }
@@ -561,8 +559,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != failure_project_permissions { if p != failure_project_permissions {
return Err(format!( return Err(format!(
"Test 3 failed. Expected {:?}, got {:?}", "Test 3 failed. Expected {failure_project_permissions:?}, got {p:?}"
failure_project_permissions, p
)); ));
} }
@@ -607,8 +604,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != success_permissions { if p != success_permissions {
return Err(format!( return Err(format!(
"Test 4 failed. Expected {:?}, got {:?}", "Test 4 failed. Expected {success_permissions:?}, got {p:?}"
success_permissions, p
)); ));
} }
@@ -666,8 +662,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != failure_project_permissions { if p != failure_project_permissions {
return Err(format!( return Err(format!(
"Test 5 failed. Expected {:?}, got {:?}", "Test 5 failed. Expected {failure_project_permissions:?}, got {p:?}"
failure_project_permissions, p
)); ));
} }
@@ -721,8 +716,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != success_permissions { if p != success_permissions {
return Err(format!( return Err(format!(
"Test 6 failed. Expected {:?}, got {:?}", "Test 6 failed. Expected {success_permissions:?}, got {p:?}"
success_permissions, p
)); ));
} }
@@ -790,8 +784,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != failure_project_permissions { if p != failure_project_permissions {
return Err(format!( return Err(format!(
"Test 7 failed. Expected {:?}, got {:?}", "Test 7 failed. Expected {failure_project_permissions:?}, got {p:?}"
failure_project_permissions, p
)); ));
} }
@@ -856,8 +849,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != success_permissions { if p != success_permissions {
return Err(format!( return Err(format!(
"Test 8 failed. Expected {:?}, got {:?}", "Test 8 failed. Expected {success_permissions:?}, got {p:?}"
success_permissions, p
)); ));
} }
@@ -927,8 +919,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != OrganizationPermissions::empty() { if p != OrganizationPermissions::empty() {
return Err(format!( return Err(format!(
"Test 1 failed. Expected no permissions, got {:?}", "Test 1 failed. Expected no permissions, got {p:?}"
p
)); ));
} }
Ok(()) Ok(())
@@ -976,8 +967,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != failure_organization_permissions { if p != failure_organization_permissions {
return Err(format!( return Err(format!(
"Test 2 failed. Expected {:?}, got {:?}", "Test 2 failed. Expected {failure_organization_permissions:?}, got {p:?}"
failure_organization_permissions, p
)); ));
} }
Ok(()) Ok(())
@@ -1021,8 +1011,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
.await; .await;
if p != success_permissions { if p != success_permissions {
return Err(format!( return Err(format!(
"Test 3 failed. Expected {:?}, got {:?}", "Test 3 failed. Expected {success_permissions:?}, got {p:?}"
success_permissions, p
)); ));
} }
Ok(()) Ok(())

View File

@@ -487,7 +487,7 @@ async fn test_multi_get_redis_cache() {
// Create 5 modpacks // Create 5 modpacks
let mut modpacks = Vec::new(); let mut modpacks = Vec::new();
for i in 0..5 { 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( let creation_data = get_public_project_creation_data(
&slug, &slug,
@@ -503,7 +503,7 @@ async fn test_multi_get_redis_cache() {
// Create 5 mods // Create 5 mods
let mut mods = Vec::new(); let mut mods = Vec::new();
for i in 0..5 { 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( let creation_data = get_public_project_creation_data(
&slug, &slug,

View File

@@ -28,7 +28,7 @@ async fn oauth_flow_happy_path() {
} = &env.dummy.oauth_client_alpha; } = &env.dummy.oauth_client_alpha;
// Initiate authorization // 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 original_state = "1234";
let resp = env let resp = env
.api .api

View File

@@ -81,7 +81,7 @@ pub async fn pat_full_test() {
// Change scopes and test again // Change scopes and test again
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.set_json(json!({ .set_json(json!({
"scopes": 0, "scopes": 0,
@@ -93,7 +93,7 @@ pub async fn pat_full_test() {
// Change scopes back, and set expiry to the past, and test again // Change scopes back, and set expiry to the past, and test again
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.set_json(json!({ .set_json(json!({
"scopes": Scopes::COLLECTION_CREATE, "scopes": Scopes::COLLECTION_CREATE,
@@ -109,21 +109,21 @@ pub async fn pat_full_test() {
// Change everything back to normal and test again // Change everything back to normal and test again
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.set_json(json!({ .set_json(json!({
"expires": Utc::now() + Duration::days(1), // no longer expired! "expires": Utc::now() + Duration::days(1), // no longer expired!
})) }))
.to_request(); .to_request();
println!("PAT ID FOR TEST: {}", id); println!("PAT ID FOR TEST: {id}");
let resp = test_env.call(req).await; let resp = test_env.call(req).await;
assert_status!(&resp, StatusCode::NO_CONTENT); assert_status!(&resp, StatusCode::NO_CONTENT);
assert_eq!(mock_pat_test(access_token).await, 200); // Works again assert_eq!(mock_pat_test(access_token).await, 200); // Works again
// Patching to a bad expiry should fail // Patching to a bad expiry should fail
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.set_json(json!({ .set_json(json!({
"expires": Utc::now() - Duration::days(1), // Past "expires": Utc::now() - Duration::days(1), // Past
@@ -140,7 +140,7 @@ pub async fn pat_full_test() {
} }
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.set_json(json!({ .set_json(json!({
"scopes": scope.bits(), "scopes": scope.bits(),
@@ -156,7 +156,7 @@ pub async fn pat_full_test() {
// Delete PAT // Delete PAT
let req = test::TestRequest::delete() let req = test::TestRequest::delete()
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.to_request(); .to_request();
let resp = test_env.call(req).await; let resp = test_env.call(req).await;
assert_status!(&resp, StatusCode::NO_CONTENT); assert_status!(&resp, StatusCode::NO_CONTENT);
@@ -260,7 +260,7 @@ pub async fn bad_pats() {
// Patching to a bad expiry should fail // Patching to a bad expiry should fail
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.set_json(json!({ .set_json(json!({
"expires": Utc::now() - Duration::days(1), // Past "expires": Utc::now() - Duration::days(1), // Past
@@ -277,7 +277,7 @@ pub async fn bad_pats() {
} }
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/_internal/pat/{}", id)) .uri(&format!("/_internal/pat/{id}"))
.append_pat(USER_USER_PAT) .append_pat(USER_USER_PAT)
.set_json(json!({ .set_json(json!({
"scopes": scope.bits(), "scopes": scope.bits(),

View File

@@ -114,7 +114,7 @@ async fn search_projects() {
let num_hits = projects.total_hits; let num_hits = projects.total_hits;
expected_project_ids.sort(); expected_project_ids.sort();
found_project_ids.sort(); found_project_ids.sort();
println!("Facets: {:?}", facets); println!("Facets: {facets:?}");
assert_eq!(found_project_ids, expected_project_ids); assert_eq!(found_project_ids, expected_project_ids);
assert_eq!(num_hits, { expected_project_ids.len() }); assert_eq!(num_hits, { expected_project_ids.len() });
} }

View File

@@ -328,7 +328,7 @@ async fn search_projects() {
.collect(); .collect();
expected_project_ids.sort(); expected_project_ids.sort();
found_project_ids.sort(); found_project_ids.sort();
println!("Facets: {:?}", facets); println!("Facets: {facets:?}");
assert_eq!(found_project_ids, expected_project_ids); assert_eq!(found_project_ids, expected_project_ids);
} }
}) })

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11,\n $12, $13, $14,\n $15, $16,\n $17, jsonb($18), jsonb($19),\n $20, $21, $22, $23,\n $24, $25, $26,\n $27\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n\n created = $12,\n modified = $13,\n last_played = $14,\n\n submitted_time_played = $15,\n recent_time_played = $16,\n\n override_java_path = $17,\n override_extra_launch_args = jsonb($18),\n override_custom_env_vars = jsonb($19),\n override_mc_memory_max = $20,\n override_mc_force_fullscreen = $21,\n override_mc_game_resolution_x = $22,\n override_mc_game_resolution_y = $23,\n\n override_hook_pre_launch = $24,\n override_hook_wrapper = $25,\n override_hook_post_exit = $26,\n\n protocol_version = $27\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 27
},
"nullable": []
},
"hash": "06368b9c4d9d386e9ba03ca91bced46853d4e8b369d5f97303241993d9d3a8e3"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11,\n $12, $13, $14,\n $15, $16,\n $17, jsonb($18), jsonb($19),\n $20, $21, $22, $23,\n $24, $25, $26,\n $27, $28\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n\n created = $12,\n modified = $13,\n last_played = $14,\n\n submitted_time_played = $15,\n recent_time_played = $16,\n\n override_java_path = $17,\n override_extra_launch_args = jsonb($18),\n override_custom_env_vars = jsonb($19),\n override_mc_memory_max = $20,\n override_mc_force_fullscreen = $21,\n override_mc_game_resolution_x = $22,\n override_mc_game_resolution_y = $23,\n\n override_hook_pre_launch = $24,\n override_hook_wrapper = $25,\n override_hook_post_exit = $26,\n\n protocol_version = $27,\n launcher_feature_version = $28\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 28
},
"nullable": []
},
"hash": "27283e20fc86c941c7d6d09259d8f4ec2e248f751f98140f77bea4f9d5971ef1"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT\n path, install_stage, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))", "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -14,129 +14,134 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "name", "name": "launcher_feature_version",
"ordinal": 2, "ordinal": 2,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "icon_path", "name": "name",
"ordinal": 3, "ordinal": 3,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "game_version", "name": "icon_path",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "protocol_version", "name": "game_version",
"ordinal": 5, "ordinal": 5,
"type_info": "Text"
},
{
"name": "protocol_version",
"ordinal": 6,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "mod_loader", "name": "mod_loader",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "mod_loader_version",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "groups!: serde_json::Value", "name": "mod_loader_version",
"ordinal": 8, "ordinal": 8,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"ordinal": 9,
"type_info": "Null" "type_info": "Null"
}, },
{ {
"name": "linked_project_id", "name": "linked_project_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "linked_version_id",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "locked", "name": "linked_version_id",
"ordinal": 11, "ordinal": 11,
"type_info": "Integer" "type_info": "Text"
}, },
{ {
"name": "created", "name": "locked",
"ordinal": 12, "ordinal": 12,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "modified", "name": "created",
"ordinal": 13, "ordinal": 13,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "last_played", "name": "modified",
"ordinal": 14, "ordinal": 14,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "submitted_time_played", "name": "last_played",
"ordinal": 15, "ordinal": 15,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "recent_time_played", "name": "submitted_time_played",
"ordinal": 16, "ordinal": 16,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_java_path", "name": "recent_time_played",
"ordinal": 17, "ordinal": 17,
"type_info": "Integer"
},
{
"name": "override_java_path",
"ordinal": 18,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "override_extra_launch_args!: serde_json::Value", "name": "override_extra_launch_args!: serde_json::Value",
"ordinal": 18,
"type_info": "Null"
},
{
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 19, "ordinal": 19,
"type_info": "Null" "type_info": "Null"
}, },
{ {
"name": "override_mc_memory_max", "name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 20, "ordinal": 20,
"type_info": "Integer" "type_info": "Null"
}, },
{ {
"name": "override_mc_force_fullscreen", "name": "override_mc_memory_max",
"ordinal": 21, "ordinal": 21,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_mc_game_resolution_x", "name": "override_mc_force_fullscreen",
"ordinal": 22, "ordinal": 22,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_mc_game_resolution_y", "name": "override_mc_game_resolution_x",
"ordinal": 23, "ordinal": 23,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_hook_pre_launch", "name": "override_mc_game_resolution_y",
"ordinal": 24, "ordinal": 24,
"type_info": "Text" "type_info": "Integer"
}, },
{ {
"name": "override_hook_wrapper", "name": "override_hook_pre_launch",
"ordinal": 25, "ordinal": 25,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "override_hook_post_exit", "name": "override_hook_wrapper",
"ordinal": 26, "ordinal": 26,
"type_info": "Text" "type_info": "Text"
},
{
"name": "override_hook_post_exit",
"ordinal": 27,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -146,6 +151,7 @@
false, false,
false, false,
false, false,
false,
true, true,
false, false,
true, true,
@@ -172,5 +178,5 @@
true true
] ]
}, },
"hash": "1b9181a1f130a097ef016aec5d14e69cc86189d182f04ae50ef8f894053d93cb" "hash": "6a434cc55635b6e325e9e5f06d21b787af281db4402c8ff45fe6a77b5be6c929"
} }

View File

@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n SELECT display_status\n FROM attached_world_data\n WHERE profile_path = $1 and world_type = $2 and world_id = $3\n ",
"describe": {
"columns": [
{
"name": "display_status",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false
]
},
"hash": "a2184fc5d62570aec0a15c0a8d628a597e90c2bf7ce5dc1b39edb6977e2f6da6"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT\n path, install_stage, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1", "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -14,129 +14,134 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "name", "name": "launcher_feature_version",
"ordinal": 2, "ordinal": 2,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "icon_path", "name": "name",
"ordinal": 3, "ordinal": 3,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "game_version", "name": "icon_path",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "protocol_version", "name": "game_version",
"ordinal": 5, "ordinal": 5,
"type_info": "Text"
},
{
"name": "protocol_version",
"ordinal": 6,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "mod_loader", "name": "mod_loader",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "mod_loader_version",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "groups!: serde_json::Value", "name": "mod_loader_version",
"ordinal": 8, "ordinal": 8,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"ordinal": 9,
"type_info": "Null" "type_info": "Null"
}, },
{ {
"name": "linked_project_id", "name": "linked_project_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "linked_version_id",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "locked", "name": "linked_version_id",
"ordinal": 11, "ordinal": 11,
"type_info": "Integer" "type_info": "Text"
}, },
{ {
"name": "created", "name": "locked",
"ordinal": 12, "ordinal": 12,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "modified", "name": "created",
"ordinal": 13, "ordinal": 13,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "last_played", "name": "modified",
"ordinal": 14, "ordinal": 14,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "submitted_time_played", "name": "last_played",
"ordinal": 15, "ordinal": 15,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "recent_time_played", "name": "submitted_time_played",
"ordinal": 16, "ordinal": 16,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_java_path", "name": "recent_time_played",
"ordinal": 17, "ordinal": 17,
"type_info": "Integer"
},
{
"name": "override_java_path",
"ordinal": 18,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "override_extra_launch_args!: serde_json::Value", "name": "override_extra_launch_args!: serde_json::Value",
"ordinal": 18,
"type_info": "Null"
},
{
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 19, "ordinal": 19,
"type_info": "Null" "type_info": "Null"
}, },
{ {
"name": "override_mc_memory_max", "name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 20, "ordinal": 20,
"type_info": "Integer" "type_info": "Null"
}, },
{ {
"name": "override_mc_force_fullscreen", "name": "override_mc_memory_max",
"ordinal": 21, "ordinal": 21,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_mc_game_resolution_x", "name": "override_mc_force_fullscreen",
"ordinal": 22, "ordinal": 22,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_mc_game_resolution_y", "name": "override_mc_game_resolution_x",
"ordinal": 23, "ordinal": 23,
"type_info": "Integer" "type_info": "Integer"
}, },
{ {
"name": "override_hook_pre_launch", "name": "override_mc_game_resolution_y",
"ordinal": 24, "ordinal": 24,
"type_info": "Text" "type_info": "Integer"
}, },
{ {
"name": "override_hook_wrapper", "name": "override_hook_pre_launch",
"ordinal": 25, "ordinal": 25,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "override_hook_post_exit", "name": "override_hook_wrapper",
"ordinal": 26, "ordinal": 26,
"type_info": "Text" "type_info": "Text"
},
{
"name": "override_hook_post_exit",
"ordinal": 27,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -146,6 +151,7 @@
false, false,
false, false,
false, false,
false,
true, true,
false, false,
true, true,
@@ -172,5 +178,5 @@
true true
] ]
}, },
"hash": "30f436efc20582d160f9485ebfff6d3ababcde700fa4cf8324d87fa2181fc47d" "hash": "c108849d77c7627d6a11d5be34984938c41283e6091a1301fc3ca0b355ffcfa9"
} }

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO attached_world_data (profile_path, world_type, world_id, display_status)\nVALUES ($1, $2, $3, $4)\nON CONFLICT (profile_path, world_type, world_id) DO UPDATE\n SET display_status = $4",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "df600f2615979ab61bfe235a04add18a4900021ee6ccfc165c9a6dad41046cba"
}

View File

@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT world_type, world_id, display_status\n FROM attached_world_data\n WHERE profile_path = $1\n ",
"describe": {
"columns": [
{
"name": "world_type",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "world_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "display_status",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "fd834e256e142820f25305ccffaf07f736c5772045b973dcc10573b399111344"
}

View File

@@ -19,6 +19,7 @@ flate2 = "1.0.28"
tempfile = "3.5.0" tempfile = "3.5.0"
dashmap = { version = "6.0.1", features = ["serde"] } dashmap = { version = "6.0.1", features = ["serde"] }
quick-xml = { version = "0.37", features = ["async-tokio"] } quick-xml = { version = "0.37", features = ["async-tokio"] }
enumset = "1.1"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }
daedalus = { path = "../../packages/daedalus" } daedalus = { path = "../../packages/daedalus" }
@@ -47,6 +48,7 @@ tokio-util = "0.7"
async-recursion = "1.0.4" async-recursion = "1.0.4"
fs4 = { version = "0.13", features = ["tokio"] } fs4 = { version = "0.13", features = ["tokio"] }
async-walkdir = "2.1" async-walkdir = "2.1"
async-compression = { version = "0.4", default-features = false, features = ["tokio", "gzip"] }
notify = { version = "6.1.1", default-features = false } notify = { version = "6.1.1", default-features = false }
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify-debouncer-mini = { version = "0.4.1", default-features = false }

View File

@@ -0,0 +1 @@
ALTER TABLE profiles ADD COLUMN launcher_feature_version TEXT NOT NULL DEFAULT 'none'

View File

@@ -0,0 +1,10 @@
CREATE TABLE attached_world_data (
profile_path TEXT NOT NULL,
world_type TEXT CHECK ( world_type in ('singleplayer', 'server') ) NOT NULL,
world_id TEXT NOT NULL,
display_status TEXT NOT NULL DEFAULT 'normal',
PRIMARY KEY (profile_path, world_type, world_id),
FOREIGN KEY (profile_path) REFERENCES profiles(path) ON DELETE CASCADE
);
CREATE INDEX attached_world_data_profile_path ON attached_world_data(profile_path);

View File

@@ -42,8 +42,8 @@ impl CensoredString {
pub fn censor(mut s: String, credentials_set: &Vec<Credentials>) -> Self { pub fn censor(mut s: String, credentials_set: &Vec<Credentials>) -> Self {
let username = whoami::username(); let username = whoami::username();
s = s s = s
.replace(&format!("/{}/", username), "/{COMPUTER_USERNAME}/") .replace(&format!("/{username}/"), "/{COMPUTER_USERNAME}/")
.replace(&format!("\\{}\\", username), "\\{COMPUTER_USERNAME}\\"); .replace(&format!("\\{username}\\"), "\\{COMPUTER_USERNAME}\\");
for credentials in credentials_set { for credentials in credentials_set {
s = s s = s
.replace(&credentials.access_token, "{MINECRAFT_ACCESS_TOKEN}") .replace(&credentials.access_token, "{MINECRAFT_ACCESS_TOKEN}")

View File

@@ -30,7 +30,7 @@ pub async fn get_loader_versions(loader: &str) -> crate::Result<Manifest> {
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
crate::ErrorKind::NoValueFor(format!("{} loader versions", loader)) crate::ErrorKind::NoValueFor(format!("{loader} loader versions"))
})?; })?;
Ok(loaders.manifest) Ok(loaders.manifest)

View File

@@ -162,7 +162,7 @@ pub async fn import_atlauncher(
profile_path: profile_path.to_string(), profile_path: profile_path.to_string(),
}; };
let backup_name = format!("ATLauncher-{}", instance_folder); let backup_name = format!("ATLauncher-{instance_folder}");
let minecraft_folder = atlauncher_instance_path; let minecraft_folder = atlauncher_instance_path;
import_atlauncher_unmanaged( import_atlauncher_unmanaged(
@@ -190,8 +190,7 @@ async fn import_atlauncher_unmanaged(
let mod_loader: ModLoader = serde_json::from_str::<ModLoader>(&mod_loader) let mod_loader: ModLoader = serde_json::from_str::<ModLoader>(&mod_loader)
.map_err(|_| { .map_err(|_| {
crate::ErrorKind::InputError(format!( crate::ErrorKind::InputError(format!(
"Could not parse mod loader type: {}", "Could not parse mod loader type: {mod_loader}"
mod_loader
)) ))
})?; })?;

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