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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
123 changed files with 1622 additions and 744 deletions

26
Cargo.lock generated
View File

@ -2604,6 +2604,28 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "enumset"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293"
dependencies = [
"enumset_derive",
"serde",
]
[[package]]
name = "enumset_derive"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242"
dependencies = [
"darling 0.20.10",
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "env_filter"
version = "0.1.3"
@ -9239,6 +9261,7 @@ name = "theseus"
version = "0.9.4"
dependencies = [
"ariadne",
"async-compression",
"async-recursion",
"async-tungstenite",
"async-walkdir",
@ -9253,6 +9276,7 @@ dependencies = [
"discord-rich-presence",
"dunce",
"either",
"enumset",
"flate2",
"fs4",
"futures",
@ -9300,6 +9324,7 @@ dependencies = [
"daedalus",
"dashmap 6.1.0",
"either",
"enumset",
"native-dialog",
"objc",
"opener",
@ -9333,6 +9358,7 @@ dependencies = [
name = "theseus_playground"
version = "0.0.0"
dependencies = [
"enumset",
"theseus",
"tokio",
"tracing",

View File

@ -24,7 +24,7 @@ import { useLoading, useTheming } from '@/store/state'
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
import AccountsCard from '@/components/ui/AccountsCard.vue'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { get } from '@/helpers/settings'
import { get } from '@/helpers/settings.ts'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'

View File

@ -7,7 +7,7 @@
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('https://cdn.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
}
@font-face {
@ -15,7 +15,7 @@
font-style: italic;
font-display: swap;
font-weight: 400;
src: url('https://cdn.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
}
@font-face {
@ -23,7 +23,7 @@
font-style: normal;
font-display: swap;
font-weight: 600;
src: url('https://cdn.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
}
@font-face {
@ -31,7 +31,7 @@
font-style: italic;
font-display: swap;
font-weight: 600;
src: url('https://cdn.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
}
.font-minecraft {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { handleError } from '@/store/notifications'
import { defineMessages, useVIntl } from '@vintl/vintl'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { get_max_memory } from '@/helpers/jre'
import { get } from '@/helpers/settings'
import { get } from '@/helpers/settings.ts'
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
const { formatMessage } = useVIntl()

View File

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

View File

@ -22,7 +22,7 @@ import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/
import { useTheming } from '@/store/state'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get, set } from '@/helpers/settings'
import { get, set } from '@/helpers/settings.ts'
const themeStore = useTheming()

View File

@ -2,7 +2,7 @@
import { ref } from 'vue'
import { ConfirmModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
import { useTheming } from '@/store/theme.ts'
const themeStore = useTheming()

View File

@ -2,7 +2,7 @@
import { ref } from 'vue'
import { NewModal as Modal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
import { useTheming } from '@/store/theme.ts'
const themeStore = useTheming()

View File

@ -2,7 +2,7 @@
import { ref } from 'vue'
import { ShareModal } from '@modrinth/ui'
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.js'
import { useTheming } from '@/store/theme.ts'
const themeStore = useTheming()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -153,7 +153,7 @@ onUnmounted(() => {
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
<router-link
class="inline-flex items-center gap-1 truncate hover:underline text-secondary"
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
:to="`/project/${modpack.id}`"
>
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />

View File

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

View File

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

View File

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

View File

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

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

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 ContentFile = {
hash: string
file_name: string
size: number
metadata?: FileMetadata
update_version_id?: string
project_type: ContentFileProjectType
}
type FileMetadata = {
project_id: string
version_id: string
}
type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
type CacheBehaviour =
// Serve expired data. If fetch fails / launcher is offline, errors are ignored
| 'stale_while_revalidate_skip_offline'
// Serve expired data, revalidate in background
| 'stale_while_revalidate'
// Must revalidate if data is expired
| 'must_revalidate'
// Ignore cache- always fetch updated data from origin
| 'bypass'
type MemorySettings = {
maximum: number
}
@ -88,6 +114,7 @@ type AppSettings = {
collapsed_navigation: boolean
advanced_rendering: boolean
native_decorations: boolean
worlds_in_home: boolean
telemetry: boolean
discord_rpc: boolean

View File

@ -9,8 +9,13 @@ type BaseWorld = {
name: string
last_played?: string
icon?: string
display_status: DisplayStatus
type: WorldType
}
export type WorldType = 'singleplayer' | 'server'
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
export type SingleplayerWorld = BaseWorld & {
type: 'singleplayer'
path: string
@ -70,8 +75,11 @@ export type ServerData = {
renderedMotd?: string
}
export async function get_recent_worlds(limit: number): Promise<WorldWithProfile[]> {
return await invoke('plugin:worlds|get_recent_worlds', { limit })
export async function get_recent_worlds(
limit: number,
displayStatuses?: DisplayStatus[],
): Promise<WorldWithProfile[]> {
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
}
export async function get_profile_worlds(path: string): Promise<World[]> {
@ -85,6 +93,20 @@ export async function get_singleplayer_world(
return await invoke('plugin:worlds|get_singleplayer_world', { instance, world })
}
export async function set_world_display_status(
instance: string,
worldType: WorldType,
worldId: string,
displayStatus: DisplayStatus,
): Promise<void> {
return await invoke('plugin:worlds|set_world_display_status', {
instance,
worldType,
worldId,
displayStatus,
})
}
export async function rename_world(
instance: string,
world: string,
@ -230,12 +252,14 @@ export async function refreshServers(
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
const newWorld = await get_singleplayer_world(instancePath, worldPath)
if (index !== -1) {
worlds[index] = await get_singleplayer_world(instancePath, worldPath)
sortWorlds(worlds)
worlds[index] = newWorld
} else {
console.error(`Error refreshing world, could not find world at path ${worldPath}.`)
console.info(`Adding new world at path: ${worldPath}.`)
worlds.push(newWorld)
}
sortWorlds(worlds)
}
export async function handleDefaultProfileUpdateEvent(

View File

@ -22,7 +22,7 @@
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
<button
v-for="filter in filterOptions"
:key="filter"
:key="`content-filter-${filter.id}`"
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
@click="toggleArray(selectedFilters, filter.id)"
>
@ -47,7 +47,7 @@
path: x.path,
disabled: x.disabled,
filename: x.file_name,
icon: x.icon,
icon: x.icon ?? undefined,
title: x.name,
data: x,
}
@ -156,7 +156,7 @@
color-fill="text"
hover-color-fill="text"
>
<button class="w-max" :disabled="installing" @click="modpackVersionModal.show()">
<button class="w-max" :disabled="installing" @click="modpackVersionModal?.show()">
<DownloadIcon /> Update pack
</button>
</ButtonStyled>
@ -170,7 +170,7 @@
>
<button
v-tooltip="`Update`"
:disabled="(item.data as any).updating"
:disabled="(item.data as ProjectListEntry).updating"
@click="updateProject(item.data)"
>
<DownloadIcon />
@ -276,6 +276,7 @@ import {
RadialHeader,
Toggle,
} from '@modrinth/ui'
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
import { formatProjectType } from '@modrinth/utils'
import type { ComputedRef } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
@ -305,43 +306,18 @@ import { profile_listener } from '@/helpers/events.js'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import dayjs from 'dayjs'
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
import type ContextMenu from '@/components/ui/ContextMenu.vue'
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
const props = defineProps({
instance: {
type: Object,
default() {
return {}
},
},
options: {
type: Object,
default() {
return {}
},
},
offline: {
type: Boolean,
default() {
return false
},
},
playing: {
type: Boolean,
default() {
return false
},
},
versions: {
type: Array,
required: true,
},
installed: {
type: Boolean,
default() {
return false
},
},
})
const props = defineProps<{
instance: GameInstance
options: InstanceType<typeof ContextMenu>
offline: boolean
playing: boolean
versions: Version[]
installed: boolean
}>()
type ProjectListEntryAuthor = {
name: string
@ -356,13 +332,15 @@ type ProjectListEntry = {
author: ProjectListEntryAuthor | null
version: string | null
file_name: string
icon: string | null
icon: string | undefined
disabled: boolean
updateVersion?: string
outdated: boolean
updated: dayjs.Dayjs
project_type: string
id?: string
updating?: boolean
selected?: boolean
}
const isPackLocked = computed(() => {
@ -375,17 +353,20 @@ const canUpdatePack = computed(() => {
const exportModal = ref(null)
const projects = ref<ProjectListEntry[]>([])
const selectedFiles = ref([])
const selectedFiles = ref<string[]>([])
const selectedProjects = computed(() =>
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
)
const selectionMap = ref(new Map())
const initProjects = async (cacheBehaviour?) => {
const initProjects = async (cacheBehaviour?: CacheBehaviour) => {
const newProjects: ProjectListEntry[] = []
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
const profileProjects = (await get_projects(props.instance.path, cacheBehaviour)) as Record<
string,
ContentFile
>
const fetchProjects = []
const fetchVersions = []
@ -397,21 +378,21 @@ const initProjects = async (cacheBehaviour?) => {
}
const [modrinthProjects, modrinthVersions] = await Promise.all([
await get_project_many(fetchProjects).catch(handleError),
await get_version_many(fetchVersions).catch(handleError),
(await get_project_many(fetchProjects).catch(handleError)) as Project[],
(await get_version_many(fetchVersions).catch(handleError)) as Version[],
])
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError),
await get_organization_many(
(await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError)) as TeamMember[][],
(await get_organization_many(
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
).catch(handleError),
).catch(handleError)) as Organization[],
])
for (const [path, file] of Object.entries(profileProjects)) {
if (file.metadata) {
const project = modrinthProjects.find((x) => file.metadata.project_id === x.id)
const version = modrinthVersions.find((x) => file.metadata.version_id === x.id)
const project = modrinthProjects.find((x) => file.metadata?.project_id === x.id)
const version = modrinthVersions.find((x) => file.metadata?.version_id === x.id)
if (project && version) {
const org = project.organization
@ -420,7 +401,7 @@ const initProjects = async (cacheBehaviour?) => {
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
let author: ProjectListEntryAuthor | null
let author: ProjectListEntryAuthor | null = null
if (org) {
author = {
name: org.name,
@ -429,13 +410,13 @@ const initProjects = async (cacheBehaviour?) => {
}
} else if (team) {
const teamMember = team.find((x) => x.is_owner)
author = {
name: teamMember.user.username,
slug: teamMember.user.username,
type: 'user',
if (teamMember) {
author = {
name: teamMember.user.username,
slug: teamMember.user.username,
type: 'user',
}
}
} else {
author = null
}
newProjects.push({
@ -464,7 +445,7 @@ const initProjects = async (cacheBehaviour?) => {
author: null,
version: null,
file_name: file.file_name,
icon: null,
icon: undefined,
disabled: file.file_name.endsWith('.disabled'),
outdated: false,
updated: dayjs(0),
@ -488,7 +469,7 @@ const initProjects = async (cacheBehaviour?) => {
}
await initProjects()
const modpackVersionModal = ref(null)
const modpackVersionModal = ref<InstanceType<typeof ModpackVersionModal> | null>()
const installing = computed(() => props.instance.install_stage !== 'installed')
const vintl = useVIntl()
@ -513,7 +494,7 @@ const messages = defineMessages({
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
const options: FilterOption[] = []
const frequency = projects.value.reduce((map, item) => {
const frequency = projects.value.reduce((map: Record<string, number>, item) => {
map[item.project_type] = (map[item.project_type] || 0) + 1
return map
}, {})
@ -544,7 +525,7 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
return options
})
const selectedFilters = ref([])
const selectedFilters = ref<string[]>([])
const filteredProjects = computed(() => {
const updatesFilter = selectedFilters.value.includes('updates')
const disabledFilter = selectedFilters.value.includes('disabled')
@ -571,7 +552,7 @@ watch(filterOptions, () => {
}
})
function toggleArray(array, value) {
function toggleArray<T>(array: T[], value: T) {
if (array.includes(value)) {
array.splice(array.indexOf(value), 1)
} else {
@ -581,7 +562,7 @@ function toggleArray(array, value) {
const searchFilter = ref('')
const selectAll = ref(false)
const shareModal = ref(null)
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
const ascending = ref(true)
const sortColumn = ref('Name')
const currentPage = ref(1)
@ -622,7 +603,7 @@ const search = computed(() => {
watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1))
const sortProjects = (filter) => {
const sortProjects = (filter: string) => {
if (sortColumn.value === filter) {
ascending.value = !ascending.value
} else {
@ -640,7 +621,7 @@ const updateAll = async () => {
}
}
const paths = await update_all(props.instance.path).catch(handleError)
const paths = (await update_all(props.instance.path).catch(handleError)) as Record<string, string>
for (const [oldVal, newVal] of Object.entries(paths)) {
const index = projects.value.findIndex((x) => x.path === oldVal)
@ -649,7 +630,7 @@ const updateAll = async () => {
if (projects.value[index].updateVersion) {
projects.value[index].version = projects.value[index].updateVersion.version_number
projects.value[index].updateVersion = null
projects.value[index].updateVersion = undefined
}
}
for (const project of setProjects) {
@ -664,15 +645,15 @@ const updateAll = async () => {
})
}
const updateProject = async (mod) => {
const updateProject = async (mod: ProjectListEntry) => {
mod.updating = true
await new Promise((resolve) => setTimeout(resolve, 0))
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
mod.updating = false
mod.outdated = false
mod.version = mod.updateVersion.version_number
mod.updateVersion = null
mod.version = mod.updateVersion?.version_number
mod.updateVersion = undefined
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
@ -683,15 +664,15 @@ const updateProject = async (mod) => {
})
}
const locks = {}
const locks: Record<string, string | null> = {}
const toggleDisableMod = async (mod) => {
const toggleDisableMod = async (mod: ProjectListEntry) => {
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
const lock = locks[mod.file_name]
while (lock) {
await new Promise((resolve) => {
setTimeout((_) => resolve(), 100)
setTimeout((value: unknown) => resolve(value), 100)
})
}
@ -716,20 +697,20 @@ const toggleDisableMod = async (mod) => {
locks[mod.file_name] = null
}
const removeMod = async (mod) => {
const removeMod = async (mod: ContentItem<ProjectListEntry>) => {
await remove_project(props.instance.path, mod.path).catch(handleError)
projects.value = projects.value.filter((x) => mod.path !== x.path)
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
name: mod.name,
project_type: mod.project_type,
id: mod.data.id,
name: mod.data.name,
project_type: mod.data.project_type,
})
}
const copyModLink = async (mod) => {
const copyModLink = async (mod: ContentItem<ProjectListEntry>) => {
await navigator.clipboard.writeText(
`https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`,
)
@ -744,15 +725,15 @@ const deleteSelected = async () => {
}
const shareNames = async () => {
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
await shareModal.value?.show(functionValues.value.map((x) => x.name).join('\n'))
}
const shareFileNames = async () => {
await shareModal.value.show(functionValues.value.map((x) => x.file_name).join('\n'))
await shareModal.value?.show(functionValues.value.map((x) => x.file_name).join('\n'))
}
const shareUrls = async () => {
await shareModal.value.show(
await shareModal.value?.show(
functionValues.value
.filter((x) => x.slug)
.map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`)
@ -761,7 +742,7 @@ const shareUrls = async () => {
}
const shareMarkdown = async () => {
await shareModal.value.show(
await shareModal.value?.show(
functionValues.value
.map((x) => {
if (x.slug) {
@ -826,15 +807,17 @@ const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
await initProjects()
})
const unlistenProfiles = await profile_listener(async (event) => {
if (
event.profile_path_id === props.instance.path &&
event.event === 'synced' &&
props.instance.install_stage !== 'pack_installing'
) {
await initProjects()
}
})
const unlistenProfiles = await profile_listener(
async (event: { event: string; profile_path_id: string }) => {
if (
event.profile_path_id === props.instance.path &&
event.event === 'synced' &&
props.instance.install_stage !== 'pack_installing'
) {
await initProjects()
}
},
)
onUnmounted(() => {
unlisten()

View File

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

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

View File

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

View File

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

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"] }
tokio = { version = "1", features = ["full"] }
webbrowser = "0.8.13"
enumset = "1.1"
tracing = "0.1.37"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@
"csp": {
"default-src": "'self' customprotocol: asset:",
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
"font-src": ["https://cdn-raw.modrinth.com/fonts/inter/"],
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self'",
"script-src": "https://*.posthog.com 'self'",

View File

@ -57,7 +57,7 @@ pub async fn fetch_forge(
ForgeVersion {
format_version,
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-installer.jar", loader_version),
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{loader_version}/forge-{loader_version}-installer.jar"),
raw: loader_version,
loader_version: version_split,
game_version: game_version.clone(),
@ -137,7 +137,7 @@ pub async fn fetch_neo(
Ok(ForgeVersion {
format_version: 2,
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{0}/forge-{0}-installer.jar", loader_version),
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{loader_version}/forge-{loader_version}-installer.jar"),
raw: loader_version,
loader_version: version_split,
game_version: "1.20.1".to_string(), // All NeoForge Forge versions are for 1.20.1
@ -163,7 +163,7 @@ pub async fn fetch_neo(
Ok(ForgeVersion {
format_version: 2,
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{0}/neoforge-{0}-installer.jar", loader_version),
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{loader_version}/neoforge-{loader_version}-installer.jar"),
loader_version: loader_version.clone(),
raw: loader_version,
game_version,
@ -502,7 +502,7 @@ async fn fetch(
)?;
artifact.url =
format_url(&format!("maven/{}", artifact_path));
format_url(&format!("maven/{artifact_path}"));
return Ok(lib);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -76,7 +76,7 @@ impl ApiV3 {
loader_field: &str,
) -> ServiceResponse {
let req = TestRequest::get()
.uri(&format!("/v3/loader_field?loader_field={}", loader_field))
.uri(&format!("/v3/loader_field?loader_field={loader_field}"))
.append_pat(ADMIN_USER_PAT)
.to_request();
self.call(req).await

View File

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

View File

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

View File

@ -137,7 +137,7 @@ impl TemporaryDatabase {
dotenvy::var("DATABASE_URL").expect("No database URL");
let mut template_url =
Url::parse(&url).expect("Invalid database URL");
template_url.set_path(&format!("/{}", TEMPLATE_DATABASE_NAME));
template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}"));
let pool = PgPool::connect(template_url.as_str())
.await

View File

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

View File

@ -487,7 +487,7 @@ async fn test_multi_get_redis_cache() {
// Create 5 modpacks
let mut modpacks = Vec::new();
for i in 0..5 {
let slug = format!("test-modpack-{}", i);
let slug = format!("test-modpack-{i}");
let creation_data = get_public_project_creation_data(
&slug,
@ -503,7 +503,7 @@ async fn test_multi_get_redis_cache() {
// Create 5 mods
let mut mods = Vec::new();
for i in 0..5 {
let slug = format!("test-mod-{}", i);
let slug = format!("test-mod-{i}");
let creation_data = get_public_project_creation_data(
&slug,

View File

@ -28,7 +28,7 @@ async fn oauth_flow_happy_path() {
} = &env.dummy.oauth_client_alpha;
// Initiate authorization
let redirect_uri = format!("{}?foo=bar", base_redirect_uri);
let redirect_uri = format!("{base_redirect_uri}?foo=bar");
let original_state = "1234";
let resp = env
.api

View File

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

View File

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

View File

@ -328,7 +328,7 @@ async fn search_projects() {
.collect();
expected_project_ids.sort();
found_project_ids.sort();
println!("Facets: {:?}", facets);
println!("Facets: {facets:?}");
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",
"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": {
"columns": [
{
@ -14,129 +14,134 @@
"type_info": "Text"
},
{
"name": "name",
"name": "launcher_feature_version",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "icon_path",
"name": "name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "game_version",
"name": "icon_path",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "protocol_version",
"name": "game_version",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "protocol_version",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "mod_loader",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "mod_loader_version",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"name": "mod_loader_version",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"ordinal": 9,
"type_info": "Null"
},
{
"name": "linked_project_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "linked_version_id",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "locked",
"name": "linked_version_id",
"ordinal": 11,
"type_info": "Integer"
"type_info": "Text"
},
{
"name": "created",
"name": "locked",
"ordinal": 12,
"type_info": "Integer"
},
{
"name": "modified",
"name": "created",
"ordinal": 13,
"type_info": "Integer"
},
{
"name": "last_played",
"name": "modified",
"ordinal": 14,
"type_info": "Integer"
},
{
"name": "submitted_time_played",
"name": "last_played",
"ordinal": 15,
"type_info": "Integer"
},
{
"name": "recent_time_played",
"name": "submitted_time_played",
"ordinal": 16,
"type_info": "Integer"
},
{
"name": "override_java_path",
"name": "recent_time_played",
"ordinal": 17,
"type_info": "Integer"
},
{
"name": "override_java_path",
"ordinal": 18,
"type_info": "Text"
},
{
"name": "override_extra_launch_args!: serde_json::Value",
"ordinal": 18,
"type_info": "Null"
},
{
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 19,
"type_info": "Null"
},
{
"name": "override_mc_memory_max",
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 20,
"type_info": "Integer"
"type_info": "Null"
},
{
"name": "override_mc_force_fullscreen",
"name": "override_mc_memory_max",
"ordinal": 21,
"type_info": "Integer"
},
{
"name": "override_mc_game_resolution_x",
"name": "override_mc_force_fullscreen",
"ordinal": 22,
"type_info": "Integer"
},
{
"name": "override_mc_game_resolution_y",
"name": "override_mc_game_resolution_x",
"ordinal": 23,
"type_info": "Integer"
},
{
"name": "override_hook_pre_launch",
"name": "override_mc_game_resolution_y",
"ordinal": 24,
"type_info": "Text"
"type_info": "Integer"
},
{
"name": "override_hook_wrapper",
"name": "override_hook_pre_launch",
"ordinal": 25,
"type_info": "Text"
},
{
"name": "override_hook_post_exit",
"name": "override_hook_wrapper",
"ordinal": 26,
"type_info": "Text"
},
{
"name": "override_hook_post_exit",
"ordinal": 27,
"type_info": "Text"
}
],
"parameters": {
@ -146,6 +151,7 @@
false,
false,
false,
false,
true,
false,
true,
@ -172,5 +178,5 @@
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",
"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": {
"columns": [
{
@ -14,129 +14,134 @@
"type_info": "Text"
},
{
"name": "name",
"name": "launcher_feature_version",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "icon_path",
"name": "name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "game_version",
"name": "icon_path",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "protocol_version",
"name": "game_version",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "protocol_version",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "mod_loader",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "mod_loader_version",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"name": "mod_loader_version",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"ordinal": 9,
"type_info": "Null"
},
{
"name": "linked_project_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "linked_version_id",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "locked",
"name": "linked_version_id",
"ordinal": 11,
"type_info": "Integer"
"type_info": "Text"
},
{
"name": "created",
"name": "locked",
"ordinal": 12,
"type_info": "Integer"
},
{
"name": "modified",
"name": "created",
"ordinal": 13,
"type_info": "Integer"
},
{
"name": "last_played",
"name": "modified",
"ordinal": 14,
"type_info": "Integer"
},
{
"name": "submitted_time_played",
"name": "last_played",
"ordinal": 15,
"type_info": "Integer"
},
{
"name": "recent_time_played",
"name": "submitted_time_played",
"ordinal": 16,
"type_info": "Integer"
},
{
"name": "override_java_path",
"name": "recent_time_played",
"ordinal": 17,
"type_info": "Integer"
},
{
"name": "override_java_path",
"ordinal": 18,
"type_info": "Text"
},
{
"name": "override_extra_launch_args!: serde_json::Value",
"ordinal": 18,
"type_info": "Null"
},
{
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 19,
"type_info": "Null"
},
{
"name": "override_mc_memory_max",
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 20,
"type_info": "Integer"
"type_info": "Null"
},
{
"name": "override_mc_force_fullscreen",
"name": "override_mc_memory_max",
"ordinal": 21,
"type_info": "Integer"
},
{
"name": "override_mc_game_resolution_x",
"name": "override_mc_force_fullscreen",
"ordinal": 22,
"type_info": "Integer"
},
{
"name": "override_mc_game_resolution_y",
"name": "override_mc_game_resolution_x",
"ordinal": 23,
"type_info": "Integer"
},
{
"name": "override_hook_pre_launch",
"name": "override_mc_game_resolution_y",
"ordinal": 24,
"type_info": "Text"
"type_info": "Integer"
},
{
"name": "override_hook_wrapper",
"name": "override_hook_pre_launch",
"ordinal": 25,
"type_info": "Text"
},
{
"name": "override_hook_post_exit",
"name": "override_hook_wrapper",
"ordinal": 26,
"type_info": "Text"
},
{
"name": "override_hook_post_exit",
"ordinal": 27,
"type_info": "Text"
}
],
"parameters": {
@ -146,6 +151,7 @@
false,
false,
false,
false,
true,
false,
true,
@ -172,5 +178,5 @@
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"
dashmap = { version = "6.0.1", features = ["serde"] }
quick-xml = { version = "0.37", features = ["async-tokio"] }
enumset = "1.1"
chrono = { version = "0.4.19", features = ["serde"] }
daedalus = { path = "../../packages/daedalus" }
@ -47,6 +48,7 @@ tokio-util = "0.7"
async-recursion = "1.0.4"
fs4 = { version = "0.13", features = ["tokio"] }
async-walkdir = "2.1"
async-compression = { version = "0.4", default-features = false, features = ["tokio", "gzip"] }
notify = { version = "6.1.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 {
let username = whoami::username();
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 {
s = s
.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?
.ok_or_else(|| {
crate::ErrorKind::NoValueFor(format!("{} loader versions", loader))
crate::ErrorKind::NoValueFor(format!("{loader} loader versions"))
})?;
Ok(loaders.manifest)

View File

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

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