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:
parent
4a2605bc1e
commit
3dad6b317f
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -2604,6 +2604,28 @@ dependencies = [
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumset"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293"
|
||||
dependencies = [
|
||||
"enumset_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumset_derive"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242"
|
||||
dependencies = [
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
@ -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",
|
||||
|
||||
@ -24,7 +24,7 @@ import { useLoading, useTheming } from '@/store/state'
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -15,7 +15,7 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -23,7 +23,7 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -31,7 +31,7 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||
}
|
||||
|
||||
.font-minecraft {
|
||||
|
||||
@ -14,7 +14,7 @@ import { ref, computed } from 'vue'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { cancel_directory_change } from '@/helpers/settings.js'
|
||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||
import { install } from '@/helpers/profile.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalWrapper ref="modal" header="Create instance">
|
||||
<ModalWrapper ref="modal" header="Creating an instance">
|
||||
<div class="modal-header">
|
||||
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
|
||||
</div>
|
||||
|
||||
@ -124,8 +124,11 @@ import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { ref, computed } from 'vue'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
@ -168,6 +171,9 @@ async function install() {
|
||||
installing.value = false
|
||||
emit('install', props.project.project_id ?? props.project.id)
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -13,15 +13,17 @@ const confirmModal = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
const onCreateInstance = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: (projectVal, versionIdVal, callback) => {
|
||||
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
||||
project.value = projectVal
|
||||
versionId.value = versionIdVal
|
||||
installing.value = false
|
||||
confirmModal.value.show()
|
||||
|
||||
onInstall.value = callback
|
||||
onCreateInstance.value = createInstanceCallback
|
||||
|
||||
trackEvent('PackInstallStart')
|
||||
},
|
||||
@ -36,6 +38,7 @@ async function install() {
|
||||
versionId.value,
|
||||
project.value.title,
|
||||
project.value.icon_url,
|
||||
onCreateInstance.value,
|
||||
).catch(handleError)
|
||||
trackEvent('PackInstall', {
|
||||
id: project.value.id,
|
||||
|
||||
@ -3,7 +3,7 @@ import { Checkbox } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@ -3,7 +3,7 @@ import { Checkbox, Toggle } from '@modrinth/ui'
|
||||
import { computed, ref, type Ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/
|
||||
import { useTheming } from '@/store/state'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { NewModal as Modal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ShareModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
import type { ColorTheme } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@ -24,13 +25,13 @@ watch(
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
(theme) => {
|
||||
(theme: ColorTheme) => {
|
||||
themeStore.setThemeState(theme)
|
||||
settings.theme = theme
|
||||
}
|
||||
"
|
||||
:current-theme="settings.theme"
|
||||
:theme-options="themeStore.themeOptions"
|
||||
:theme-options="themeStore.getThemeOptions()"
|
||||
system-theme-color="system"
|
||||
/>
|
||||
|
||||
@ -80,10 +81,28 @@ watch(
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="w-40"
|
||||
:options="['Home', 'Library']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
||||
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
||||
themeStore.featureFlags['worlds_in_home'] = newValue
|
||||
settings.feature_flags['worlds_in_home'] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { handleError } from '@/store/notifications'
|
||||
|
||||
@ -2,18 +2,15 @@
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const settings = ref(await get())
|
||||
const options = ref(['project_background', 'page_path', 'worlds_tab'])
|
||||
const settings = ref(await getSettings())
|
||||
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
||||
|
||||
function getStoreValue(key: string) {
|
||||
return themeStore.featureFlags[key] ?? false
|
||||
}
|
||||
|
||||
function setStoreValue(key: string, value: boolean) {
|
||||
function setFeatureFlag(key: string, value: boolean) {
|
||||
themeStore.featureFlags[key] = value
|
||||
settings.value.feature_flags[key] = value
|
||||
}
|
||||
@ -21,7 +18,7 @@ function setStoreValue(key: string, value: boolean) {
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
await setSettings(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@ -36,8 +33,8 @@ watch(
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="getStoreValue(option)"
|
||||
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
|
||||
:model-value="themeStore.getFeatureFlag(option)"
|
||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { Button, Slider } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
|
||||
@ -153,7 +153,7 @@ onUnmounted(() => {
|
||||
•
|
||||
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
||||
<router-link
|
||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary"
|
||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||
:to="`/project/${modpack.id}`"
|
||||
>
|
||||
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
||||
|
||||
@ -13,16 +13,17 @@ import {
|
||||
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||
import { watch, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch, onMounted, onUnmounted, ref, computed } from 'vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { kill } from '@/helpers/profile'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { get_all } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
|
||||
const props = defineProps<{
|
||||
recentInstances: GameInstance[]
|
||||
@ -54,7 +55,9 @@ type WorldJumpBackInItem = BaseJumpBackInItem & {
|
||||
|
||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
||||
|
||||
watch(props.recentInstances, async () => {
|
||||
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
||||
|
||||
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||
await populateJumpBackIn().catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
@ -66,66 +69,71 @@ await populateJumpBackIn().catch(() => {
|
||||
|
||||
async function populateJumpBackIn() {
|
||||
console.info('Repopulating jump back in...')
|
||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN)
|
||||
|
||||
const worldItems: WorldJumpBackInItem[] = []
|
||||
worlds.forEach((world) => {
|
||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||
|
||||
if (!instance || !world.last_played) {
|
||||
return
|
||||
}
|
||||
if (showWorlds.value) {
|
||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
||||
|
||||
worldItems.push({
|
||||
type: 'world',
|
||||
last_played: dayjs(world.last_played),
|
||||
world: world,
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
worlds.forEach((world) => {
|
||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||
|
||||
const servers: {
|
||||
instancePath: string
|
||||
address: string
|
||||
}[] = worldItems
|
||||
.filter((item) => item.world.type === 'server' && item.instance)
|
||||
.map((item) => ({
|
||||
instancePath: item.instance.path,
|
||||
address: (item.world as ServerWorld).address,
|
||||
}))
|
||||
|
||||
// fetch protocol versions for all unique MC versions with server worlds
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||
await Promise.all(
|
||||
[...uniqueServerInstances].map((path) => {
|
||||
get_profile_protocol_version(path)
|
||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||
.catch(() => {
|
||||
console.error(`Failed to get profile protocol for: ${path} `)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// initialize server data
|
||||
servers.forEach(({ address }) => {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
if (!instance || !world.last_played) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
await Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
)
|
||||
worldItems.push({
|
||||
type: 'world',
|
||||
last_played: dayjs(world.last_played),
|
||||
world: world,
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
|
||||
const servers: {
|
||||
instancePath: string
|
||||
address: string
|
||||
}[] = worldItems
|
||||
.filter((item) => item.world.type === 'server' && item.instance)
|
||||
.map((item) => ({
|
||||
instancePath: item.instance.path,
|
||||
address: (item.world as ServerWorld).address,
|
||||
}))
|
||||
|
||||
// fetch protocol versions for all unique MC versions with server worlds
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||
await Promise.all(
|
||||
[...uniqueServerInstances].map((path) =>
|
||||
get_profile_protocol_version(path)
|
||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||
.catch(() => {
|
||||
console.error(`Failed to get profile protocol for: ${path} `)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// initialize server data
|
||||
servers.forEach(({ address }) => {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const instanceItems: InstanceJumpBackInItem[] = []
|
||||
props.recentInstances.forEach((instance) => {
|
||||
if (worldItems.some((item) => item.instance.path === instance.path) || !instance.last_played) {
|
||||
return
|
||||
for (const instance of props.recentInstances) {
|
||||
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
||||
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
||||
continue
|
||||
}
|
||||
|
||||
instanceItems.push({
|
||||
@ -133,13 +141,13 @@ async function populateJumpBackIn() {
|
||||
last_played: dayjs(instance.last_played),
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
||||
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
|
||||
jumpBackInItems.value = items.filter(
|
||||
(item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO),
|
||||
)
|
||||
jumpBackInItems.value = items
|
||||
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
||||
.slice(0, MAX_JUMP_BACK_IN)
|
||||
}
|
||||
|
||||
async function refreshServer(address: string, instancePath: string) {
|
||||
@ -155,6 +163,18 @@ async function joinWorld(world: WorldWithProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
async function playInstance(instance: GameInstance) {
|
||||
await run(instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
loader: instance.loader,
|
||||
game_version: instance.game_version,
|
||||
source: 'WorldItem',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function stopInstance(path: string) {
|
||||
await kill(path).catch(handleError)
|
||||
trackEvent('InstanceStop', {
|
||||
@ -209,11 +229,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||
<HeadingLink
|
||||
v-if="(theme.featureFlags as Record<string, boolean>)['worlds_tab']"
|
||||
to="/worlds"
|
||||
class="mt-1"
|
||||
>
|
||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||
Jump back in
|
||||
</HeadingLink>
|
||||
<span
|
||||
@ -222,7 +238,7 @@ onUnmounted(() => {
|
||||
>
|
||||
Jump back in
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||
<template
|
||||
v-for="item in jumpBackInItems"
|
||||
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||
@ -246,7 +262,7 @@ onUnmounted(() => {
|
||||
:rendered-motd="
|
||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||
"
|
||||
:current-protocol="protocolVersions[item.instance.game_version]"
|
||||
:current-protocol="protocolVersions[item.instance.path]"
|
||||
:game-mode="
|
||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||
"
|
||||
@ -259,6 +275,7 @@ onUnmounted(() => {
|
||||
? refreshServer(item.world.address, item.instance.path)
|
||||
: {}
|
||||
"
|
||||
@update="() => populateJumpBackIn()"
|
||||
@play="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
@ -266,6 +283,12 @@ onUnmounted(() => {
|
||||
joinWorld(item.world)
|
||||
}
|
||||
"
|
||||
@play-instance="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
playInstance(item.instance)
|
||||
}
|
||||
"
|
||||
@stop="() => stopInstance(item.instance.path)"
|
||||
/>
|
||||
<InstanceItem v-else :instance="item.instance" />
|
||||
@ -273,3 +296,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.grid-when-huge {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
||||
import { getWorldIdentifier, showWorldInFolder } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
set_world_display_status,
|
||||
getWorldIdentifier,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import {
|
||||
IssuesIcon,
|
||||
@ -19,6 +23,7 @@ import {
|
||||
TrashIcon,
|
||||
UpdatedIcon,
|
||||
UserIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
|
||||
import type { MessageDescriptor } from '@vintl/vintl'
|
||||
@ -35,7 +40,7 @@ const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'play' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
@ -69,6 +74,7 @@ const props = withDefaults(
|
||||
playingWorld: false,
|
||||
startingInstance: false,
|
||||
supportsQuickPlay: false,
|
||||
currentProtocol: null,
|
||||
|
||||
refreshing: false,
|
||||
serverStatus: undefined,
|
||||
@ -143,10 +149,18 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.play_anyway',
|
||||
defaultMessage: 'Play anyway',
|
||||
},
|
||||
playInstance: {
|
||||
id: 'instance.worlds.play_instance',
|
||||
defaultMessage: 'Play instance',
|
||||
},
|
||||
worldInUse: {
|
||||
id: 'instance.worlds.world_in_use',
|
||||
defaultMessage: 'World is in use',
|
||||
},
|
||||
dontShowOnHome: {
|
||||
id: 'instance.worlds.dont_show_on_home',
|
||||
defaultMessage: `Don't show on Home`,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
@ -336,6 +350,12 @@ const messages = defineMessages({
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
shown: !!instancePath,
|
||||
disabled: playingInstance,
|
||||
action: () => emit('play-instance'),
|
||||
},
|
||||
{
|
||||
id: 'play-anyway',
|
||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
||||
@ -344,7 +364,7 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instancePath,
|
||||
action: () => router.push(encodeURI(`/instance/${instancePath}/worlds`)),
|
||||
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
@ -369,6 +389,24 @@ const messages = defineMessages({
|
||||
action: () =>
|
||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !!instancePath,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
shown: !!instancePath,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instancePath,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
).then(() => {
|
||||
emit('update')
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !instancePath,
|
||||
@ -385,6 +423,10 @@ const messages = defineMessages({
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #play-instance>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playInstance) }}
|
||||
</template>
|
||||
<template #play-anyway>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playAnyway) }}
|
||||
@ -406,6 +448,10 @@ const messages = defineMessages({
|
||||
<template #refresh>
|
||||
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { edit_server_in_profile, type ServerWorld } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
type ServerPackStatus,
|
||||
edit_server_in_profile,
|
||||
type ServerWorld,
|
||||
set_world_display_status,
|
||||
type DisplayStatus,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@ -21,10 +28,14 @@ const props = defineProps<{
|
||||
|
||||
const modal = ref()
|
||||
|
||||
const name = ref()
|
||||
const address = ref()
|
||||
const resourcePack = ref('enabled')
|
||||
const index = ref()
|
||||
const name = ref<string>('')
|
||||
const address = ref<string>('')
|
||||
const resourcePack = ref<ServerPackStatus>('enabled')
|
||||
const index = ref<number>(0)
|
||||
const displayStatus = ref<DisplayStatus>('normal')
|
||||
const hideFromHome = ref(false)
|
||||
|
||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||
|
||||
async function saveServer() {
|
||||
const serverName = name.value ? name.value : address.value
|
||||
@ -36,12 +47,23 @@ async function saveServer() {
|
||||
address.value,
|
||||
resourcePackStatus,
|
||||
).catch(handleError)
|
||||
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
'server',
|
||||
address.value,
|
||||
newDisplayStatus.value,
|
||||
)
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
name: serverName,
|
||||
type: 'server',
|
||||
index: index.value,
|
||||
address: address.value,
|
||||
pack_status: resourcePackStatus,
|
||||
display_status: newDisplayStatus.value,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
@ -51,6 +73,8 @@ function show(server: ServerWorld) {
|
||||
address.value = server.address
|
||||
resourcePack.value = server.pack_status
|
||||
index.value = server.index
|
||||
displayStatus.value = server.display_status
|
||||
hideFromHome.value = server.display_status === 'hidden'
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
@ -75,6 +99,7 @@ const titleMessage = defineMessage({
|
||||
v-model:address="address"
|
||||
v-model:resource-pack="resourcePack"
|
||||
/>
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!address" @click="saveServer">
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import type { SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||
import { rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
||||
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||
import { set_world_display_status, rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [path: string, name: string, removeIcon: boolean]
|
||||
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
@ -25,6 +26,10 @@ const icon = ref()
|
||||
const name = ref()
|
||||
const path = ref()
|
||||
const removeIcon = ref(false)
|
||||
const displayStatus = ref<DisplayStatus>('normal')
|
||||
const hideFromHome = ref(false)
|
||||
|
||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||
|
||||
async function saveWorld() {
|
||||
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
||||
@ -32,8 +37,16 @@ async function saveWorld() {
|
||||
if (removeIcon.value) {
|
||||
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
||||
}
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
'singleplayer',
|
||||
path.value,
|
||||
newDisplayStatus.value,
|
||||
)
|
||||
}
|
||||
|
||||
emit('submit', path.value, name.value, removeIcon.value)
|
||||
emit('submit', path.value, name.value, removeIcon.value, newDisplayStatus.value)
|
||||
hide()
|
||||
}
|
||||
|
||||
@ -41,6 +54,8 @@ function show(world: SingleplayerWorld) {
|
||||
name.value = world.name
|
||||
path.value = world.path
|
||||
icon.value = world.icon
|
||||
displayStatus.value = world.display_status
|
||||
hideFromHome.value = world.display_status === 'hidden'
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
@ -87,6 +102,7 @@ const messages = defineMessages({
|
||||
class="w-full"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { computed } from 'vue'
|
||||
import { Checkbox } from '@modrinth/ui'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const value = defineModel<boolean>({ required: true })
|
||||
|
||||
const labelMessage = defineMessage({
|
||||
id: 'instance.edit-world.hide-from-home',
|
||||
defaultMessage: `Hide from the Home page`,
|
||||
})
|
||||
|
||||
const label = computed(() => formatMessage(labelMessage))
|
||||
</script>
|
||||
<template>
|
||||
<Checkbox v-model="value" :label="label" />
|
||||
</template>
|
||||
@ -7,7 +7,13 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { create } from './profile'
|
||||
|
||||
// Installs pack from a version ID
|
||||
export async function create_profile_and_install(projectId, versionId, packTitle, iconUrl) {
|
||||
export async function create_profile_and_install(
|
||||
projectId,
|
||||
versionId,
|
||||
packTitle,
|
||||
iconUrl,
|
||||
createInstanceCallback = () => {},
|
||||
) {
|
||||
const location = {
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
@ -24,6 +30,7 @@ export async function create_profile_and_install(projectId, versionId, packTitle
|
||||
null,
|
||||
true,
|
||||
)
|
||||
createInstanceCallback(profile)
|
||||
|
||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
// Settings object
|
||||
/*
|
||||
|
||||
Settings {
|
||||
"memory": MemorySettings,
|
||||
"game_resolution": [int int],
|
||||
"custom_java_args": [String ...],
|
||||
"custom_env_args" : [(string, string) ... ]>,
|
||||
"java_globals": Hash of (string, Path),
|
||||
"default_user": Uuid string (can be null),
|
||||
"hooks": Hooks,
|
||||
"max_concurrent_downloads": uint,
|
||||
"version": u32,
|
||||
"collapsed_navigation": bool,
|
||||
}
|
||||
|
||||
Memorysettings {
|
||||
"min": u32, can be null,
|
||||
"max": u32,
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// Get full settings object
|
||||
export async function get() {
|
||||
return await invoke('plugin:settings|settings_get')
|
||||
}
|
||||
|
||||
// Set full settings object
|
||||
export async function set(settings) {
|
||||
return await invoke('plugin:settings|settings_set', { settings })
|
||||
}
|
||||
|
||||
export async function cancel_directory_change() {
|
||||
return await invoke('plugin:settings|cancel_directory_change')
|
||||
}
|
||||
78
apps/app-frontend/src/helpers/settings.ts
Normal file
78
apps/app-frontend/src/helpers/settings.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
||||
|
||||
// Settings object
|
||||
/*
|
||||
|
||||
Settings {
|
||||
"memory": MemorySettings,
|
||||
"game_resolution": [int int],
|
||||
"custom_java_args": [String ...],
|
||||
"custom_env_args" : [(string, string) ... ]>,
|
||||
"java_globals": Hash of (string, Path),
|
||||
"default_user": Uuid string (can be null),
|
||||
"hooks": Hooks,
|
||||
"max_concurrent_downloads": uint,
|
||||
"version": u32,
|
||||
"collapsed_navigation": bool,
|
||||
}
|
||||
|
||||
Memorysettings {
|
||||
"min": u32, can be null,
|
||||
"max": u32,
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
export type AppSettings = {
|
||||
max_concurrent_downloads: number
|
||||
max_concurrent_writes: number
|
||||
|
||||
theme: ColorTheme
|
||||
default_page: 'home' | 'library'
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
toggle_sidebar: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
personalized_ads: boolean
|
||||
|
||||
onboarded: boolean
|
||||
|
||||
extra_launch_args: string[]
|
||||
custom_env_vars: [string, string][]
|
||||
memory: MemorySettings
|
||||
force_fullscreen: boolean
|
||||
game_resolution: WindowSize
|
||||
hide_on_process_start: boolean
|
||||
hooks: Hooks
|
||||
|
||||
custom_dir?: string | null
|
||||
prev_custom_dir?: string | null
|
||||
migrated: boolean
|
||||
|
||||
developer_mode: boolean
|
||||
feature_flags: Record<FeatureFlag, boolean>
|
||||
}
|
||||
|
||||
// Get full settings object
|
||||
export async function get() {
|
||||
return (await invoke('plugin:settings|settings_get')) as AppSettings
|
||||
}
|
||||
|
||||
// Set full settings object
|
||||
export async function set(settings: AppSettings) {
|
||||
return await invoke('plugin:settings|settings_set', { settings })
|
||||
}
|
||||
|
||||
export async function cancel_directory_change(): Promise<void> {
|
||||
return await invoke('plugin:settings|cancel_directory_change')
|
||||
}
|
||||
27
apps/app-frontend/src/helpers/types.d.ts
vendored
27
apps/app-frontend/src/helpers/types.d.ts
vendored
@ -48,6 +48,32 @@ type LinkedData = {
|
||||
|
||||
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||
|
||||
type ContentFile = {
|
||||
hash: string
|
||||
file_name: string
|
||||
size: number
|
||||
metadata?: FileMetadata
|
||||
update_version_id?: string
|
||||
project_type: ContentFileProjectType
|
||||
}
|
||||
|
||||
type FileMetadata = {
|
||||
project_id: string
|
||||
version_id: string
|
||||
}
|
||||
|
||||
type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
|
||||
|
||||
type CacheBehaviour =
|
||||
// Serve expired data. If fetch fails / launcher is offline, errors are ignored
|
||||
| 'stale_while_revalidate_skip_offline'
|
||||
// Serve expired data, revalidate in background
|
||||
| 'stale_while_revalidate'
|
||||
// Must revalidate if data is expired
|
||||
| 'must_revalidate'
|
||||
// Ignore cache- always fetch updated data from origin
|
||||
| 'bypass'
|
||||
|
||||
type MemorySettings = {
|
||||
maximum: number
|
||||
}
|
||||
@ -88,6 +114,7 @@ type AppSettings = {
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
worlds_in_home: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
|
||||
@ -9,8 +9,13 @@ type BaseWorld = {
|
||||
name: string
|
||||
last_played?: string
|
||||
icon?: string
|
||||
display_status: DisplayStatus
|
||||
type: WorldType
|
||||
}
|
||||
|
||||
export type WorldType = 'singleplayer' | 'server'
|
||||
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
|
||||
|
||||
export type SingleplayerWorld = BaseWorld & {
|
||||
type: 'singleplayer'
|
||||
path: string
|
||||
@ -70,8 +75,11 @@ export type ServerData = {
|
||||
renderedMotd?: string
|
||||
}
|
||||
|
||||
export async function get_recent_worlds(limit: number): Promise<WorldWithProfile[]> {
|
||||
return await invoke('plugin:worlds|get_recent_worlds', { limit })
|
||||
export async function get_recent_worlds(
|
||||
limit: number,
|
||||
displayStatuses?: DisplayStatus[],
|
||||
): Promise<WorldWithProfile[]> {
|
||||
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
|
||||
}
|
||||
|
||||
export async function get_profile_worlds(path: string): Promise<World[]> {
|
||||
@ -85,6 +93,20 @@ export async function get_singleplayer_world(
|
||||
return await invoke('plugin:worlds|get_singleplayer_world', { instance, world })
|
||||
}
|
||||
|
||||
export async function set_world_display_status(
|
||||
instance: string,
|
||||
worldType: WorldType,
|
||||
worldId: string,
|
||||
displayStatus: DisplayStatus,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:worlds|set_world_display_status', {
|
||||
instance,
|
||||
worldType,
|
||||
worldId,
|
||||
displayStatus,
|
||||
})
|
||||
}
|
||||
|
||||
export async function rename_world(
|
||||
instance: string,
|
||||
world: string,
|
||||
@ -230,12 +252,14 @@ export async function refreshServers(
|
||||
|
||||
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
||||
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
||||
const newWorld = await get_singleplayer_world(instancePath, worldPath)
|
||||
if (index !== -1) {
|
||||
worlds[index] = await get_singleplayer_world(instancePath, worldPath)
|
||||
sortWorlds(worlds)
|
||||
worlds[index] = newWorld
|
||||
} else {
|
||||
console.error(`Error refreshing world, could not find world at path ${worldPath}.`)
|
||||
console.info(`Adding new world at path: ${worldPath}.`)
|
||||
worlds.push(newWorld)
|
||||
}
|
||||
sortWorlds(worlds)
|
||||
}
|
||||
|
||||
export async function handleDefaultProfileUpdateEvent(
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||
<button
|
||||
v-for="filter in filterOptions"
|
||||
:key="filter"
|
||||
:key="`content-filter-${filter.id}`"
|
||||
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
||||
@click="toggleArray(selectedFilters, filter.id)"
|
||||
>
|
||||
@ -47,7 +47,7 @@
|
||||
path: x.path,
|
||||
disabled: x.disabled,
|
||||
filename: x.file_name,
|
||||
icon: x.icon,
|
||||
icon: x.icon ?? undefined,
|
||||
title: x.name,
|
||||
data: x,
|
||||
}
|
||||
@ -156,7 +156,7 @@
|
||||
color-fill="text"
|
||||
hover-color-fill="text"
|
||||
>
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal.show()">
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal?.show()">
|
||||
<DownloadIcon /> Update pack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@ -170,7 +170,7 @@
|
||||
>
|
||||
<button
|
||||
v-tooltip="`Update`"
|
||||
:disabled="(item.data as any).updating"
|
||||
:disabled="(item.data as ProjectListEntry).updating"
|
||||
@click="updateProject(item.data)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
@ -276,6 +276,7 @@ import {
|
||||
RadialHeader,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
@ -305,43 +306,18 @@ import { profile_listener } from '@/helpers/events.js'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import dayjs from 'dayjs'
|
||||
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
offline: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
playing: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
options: InstanceType<typeof ContextMenu>
|
||||
offline: boolean
|
||||
playing: boolean
|
||||
versions: Version[]
|
||||
installed: boolean
|
||||
}>()
|
||||
|
||||
type ProjectListEntryAuthor = {
|
||||
name: string
|
||||
@ -356,13 +332,15 @@ type ProjectListEntry = {
|
||||
author: ProjectListEntryAuthor | null
|
||||
version: string | null
|
||||
file_name: string
|
||||
icon: string | null
|
||||
icon: string | undefined
|
||||
disabled: boolean
|
||||
updateVersion?: string
|
||||
outdated: boolean
|
||||
updated: dayjs.Dayjs
|
||||
project_type: string
|
||||
id?: string
|
||||
updating?: boolean
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const isPackLocked = computed(() => {
|
||||
@ -375,17 +353,20 @@ const canUpdatePack = computed(() => {
|
||||
const exportModal = ref(null)
|
||||
|
||||
const projects = ref<ProjectListEntry[]>([])
|
||||
const selectedFiles = ref([])
|
||||
const selectedFiles = ref<string[]>([])
|
||||
const selectedProjects = computed(() =>
|
||||
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
|
||||
)
|
||||
|
||||
const selectionMap = ref(new Map())
|
||||
|
||||
const initProjects = async (cacheBehaviour?) => {
|
||||
const initProjects = async (cacheBehaviour?: CacheBehaviour) => {
|
||||
const newProjects: ProjectListEntry[] = []
|
||||
|
||||
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
|
||||
const profileProjects = (await get_projects(props.instance.path, cacheBehaviour)) as Record<
|
||||
string,
|
||||
ContentFile
|
||||
>
|
||||
const fetchProjects = []
|
||||
const fetchVersions = []
|
||||
|
||||
@ -397,21 +378,21 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
|
||||
const [modrinthProjects, modrinthVersions] = await Promise.all([
|
||||
await get_project_many(fetchProjects).catch(handleError),
|
||||
await get_version_many(fetchVersions).catch(handleError),
|
||||
(await get_project_many(fetchProjects).catch(handleError)) as Project[],
|
||||
(await get_version_many(fetchVersions).catch(handleError)) as Version[],
|
||||
])
|
||||
|
||||
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
|
||||
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError),
|
||||
await get_organization_many(
|
||||
(await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError)) as TeamMember[][],
|
||||
(await get_organization_many(
|
||||
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
|
||||
).catch(handleError),
|
||||
).catch(handleError)) as Organization[],
|
||||
])
|
||||
|
||||
for (const [path, file] of Object.entries(profileProjects)) {
|
||||
if (file.metadata) {
|
||||
const project = modrinthProjects.find((x) => file.metadata.project_id === x.id)
|
||||
const version = modrinthVersions.find((x) => file.metadata.version_id === x.id)
|
||||
const project = modrinthProjects.find((x) => file.metadata?.project_id === x.id)
|
||||
const version = modrinthVersions.find((x) => file.metadata?.version_id === x.id)
|
||||
|
||||
if (project && version) {
|
||||
const org = project.organization
|
||||
@ -420,7 +401,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
|
||||
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
|
||||
|
||||
let author: ProjectListEntryAuthor | null
|
||||
let author: ProjectListEntryAuthor | null = null
|
||||
if (org) {
|
||||
author = {
|
||||
name: org.name,
|
||||
@ -429,13 +410,13 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
} else if (team) {
|
||||
const teamMember = team.find((x) => x.is_owner)
|
||||
author = {
|
||||
name: teamMember.user.username,
|
||||
slug: teamMember.user.username,
|
||||
type: 'user',
|
||||
if (teamMember) {
|
||||
author = {
|
||||
name: teamMember.user.username,
|
||||
slug: teamMember.user.username,
|
||||
type: 'user',
|
||||
}
|
||||
}
|
||||
} else {
|
||||
author = null
|
||||
}
|
||||
|
||||
newProjects.push({
|
||||
@ -464,7 +445,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
author: null,
|
||||
version: null,
|
||||
file_name: file.file_name,
|
||||
icon: null,
|
||||
icon: undefined,
|
||||
disabled: file.file_name.endsWith('.disabled'),
|
||||
outdated: false,
|
||||
updated: dayjs(0),
|
||||
@ -488,7 +469,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
await initProjects()
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
const modpackVersionModal = ref<InstanceType<typeof ModpackVersionModal> | null>()
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const vintl = useVIntl()
|
||||
@ -513,7 +494,7 @@ const messages = defineMessages({
|
||||
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
const options: FilterOption[] = []
|
||||
|
||||
const frequency = projects.value.reduce((map, item) => {
|
||||
const frequency = projects.value.reduce((map: Record<string, number>, item) => {
|
||||
map[item.project_type] = (map[item.project_type] || 0) + 1
|
||||
return map
|
||||
}, {})
|
||||
@ -544,7 +525,7 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
return options
|
||||
})
|
||||
|
||||
const selectedFilters = ref([])
|
||||
const selectedFilters = ref<string[]>([])
|
||||
const filteredProjects = computed(() => {
|
||||
const updatesFilter = selectedFilters.value.includes('updates')
|
||||
const disabledFilter = selectedFilters.value.includes('disabled')
|
||||
@ -571,7 +552,7 @@ watch(filterOptions, () => {
|
||||
}
|
||||
})
|
||||
|
||||
function toggleArray(array, value) {
|
||||
function toggleArray<T>(array: T[], value: T) {
|
||||
if (array.includes(value)) {
|
||||
array.splice(array.indexOf(value), 1)
|
||||
} else {
|
||||
@ -581,7 +562,7 @@ function toggleArray(array, value) {
|
||||
|
||||
const searchFilter = ref('')
|
||||
const selectAll = ref(false)
|
||||
const shareModal = ref(null)
|
||||
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
|
||||
const ascending = ref(true)
|
||||
const sortColumn = ref('Name')
|
||||
const currentPage = ref(1)
|
||||
@ -622,7 +603,7 @@ const search = computed(() => {
|
||||
|
||||
watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1))
|
||||
|
||||
const sortProjects = (filter) => {
|
||||
const sortProjects = (filter: string) => {
|
||||
if (sortColumn.value === filter) {
|
||||
ascending.value = !ascending.value
|
||||
} else {
|
||||
@ -640,7 +621,7 @@ const updateAll = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const paths = await update_all(props.instance.path).catch(handleError)
|
||||
const paths = (await update_all(props.instance.path).catch(handleError)) as Record<string, string>
|
||||
|
||||
for (const [oldVal, newVal] of Object.entries(paths)) {
|
||||
const index = projects.value.findIndex((x) => x.path === oldVal)
|
||||
@ -649,7 +630,7 @@ const updateAll = async () => {
|
||||
|
||||
if (projects.value[index].updateVersion) {
|
||||
projects.value[index].version = projects.value[index].updateVersion.version_number
|
||||
projects.value[index].updateVersion = null
|
||||
projects.value[index].updateVersion = undefined
|
||||
}
|
||||
}
|
||||
for (const project of setProjects) {
|
||||
@ -664,15 +645,15 @@ const updateAll = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateProject = async (mod) => {
|
||||
const updateProject = async (mod: ProjectListEntry) => {
|
||||
mod.updating = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
|
||||
mod.updating = false
|
||||
|
||||
mod.outdated = false
|
||||
mod.version = mod.updateVersion.version_number
|
||||
mod.updateVersion = null
|
||||
mod.version = mod.updateVersion?.version_number
|
||||
mod.updateVersion = undefined
|
||||
|
||||
trackEvent('InstanceProjectUpdate', {
|
||||
loader: props.instance.loader,
|
||||
@ -683,15 +664,15 @@ const updateProject = async (mod) => {
|
||||
})
|
||||
}
|
||||
|
||||
const locks = {}
|
||||
const locks: Record<string, string | null> = {}
|
||||
|
||||
const toggleDisableMod = async (mod) => {
|
||||
const toggleDisableMod = async (mod: ProjectListEntry) => {
|
||||
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
|
||||
const lock = locks[mod.file_name]
|
||||
|
||||
while (lock) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout((_) => resolve(), 100)
|
||||
setTimeout((value: unknown) => resolve(value), 100)
|
||||
})
|
||||
}
|
||||
|
||||
@ -716,20 +697,20 @@ const toggleDisableMod = async (mod) => {
|
||||
locks[mod.file_name] = null
|
||||
}
|
||||
|
||||
const removeMod = async (mod) => {
|
||||
const removeMod = async (mod: ContentItem<ProjectListEntry>) => {
|
||||
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
||||
|
||||
trackEvent('InstanceProjectRemove', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
id: mod.id,
|
||||
name: mod.name,
|
||||
project_type: mod.project_type,
|
||||
id: mod.data.id,
|
||||
name: mod.data.name,
|
||||
project_type: mod.data.project_type,
|
||||
})
|
||||
}
|
||||
|
||||
const copyModLink = async (mod) => {
|
||||
const copyModLink = async (mod: ContentItem<ProjectListEntry>) => {
|
||||
await navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`,
|
||||
)
|
||||
@ -744,15 +725,15 @@ const deleteSelected = async () => {
|
||||
}
|
||||
|
||||
const shareNames = async () => {
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
await shareModal.value?.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
}
|
||||
|
||||
const shareFileNames = async () => {
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.file_name).join('\n'))
|
||||
await shareModal.value?.show(functionValues.value.map((x) => x.file_name).join('\n'))
|
||||
}
|
||||
|
||||
const shareUrls = async () => {
|
||||
await shareModal.value.show(
|
||||
await shareModal.value?.show(
|
||||
functionValues.value
|
||||
.filter((x) => x.slug)
|
||||
.map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`)
|
||||
@ -761,7 +742,7 @@ const shareUrls = async () => {
|
||||
}
|
||||
|
||||
const shareMarkdown = async () => {
|
||||
await shareModal.value.show(
|
||||
await shareModal.value?.show(
|
||||
functionValues.value
|
||||
.map((x) => {
|
||||
if (x.slug) {
|
||||
@ -826,15 +807,17 @@ const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
await initProjects()
|
||||
})
|
||||
|
||||
const unlistenProfiles = await profile_listener(async (event) => {
|
||||
if (
|
||||
event.profile_path_id === props.instance.path &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing'
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
})
|
||||
const unlistenProfiles = await profile_listener(
|
||||
async (event: { event: string; profile_path_id: string }) => {
|
||||
if (
|
||||
event.profile_path_id === props.instance.path &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing'
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<FilterBar v-model="filters" :options="filterOptions" />
|
||||
<FilterBar v-model="filters" :options="filterOptions" show-all-options />
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<WorldItem
|
||||
v-for="world in filteredWorlds"
|
||||
@ -225,6 +225,11 @@ const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||
await refreshAllWorlds()
|
||||
|
||||
async function refreshServer(address: string) {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
}
|
||||
}
|
||||
await refreshServerData(serverData.value[address], protocolVersion.value, address)
|
||||
}
|
||||
|
||||
@ -263,9 +268,12 @@ async function addServer(server: ServerWorld) {
|
||||
async function editServer(server: ServerWorld) {
|
||||
const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index)
|
||||
if (index !== -1) {
|
||||
const oldServer = worlds.value[index] as ServerWorld
|
||||
worlds.value[index] = server
|
||||
sortWorlds(worlds.value)
|
||||
await refreshServer(server.address)
|
||||
if (oldServer.address !== server.address) {
|
||||
await refreshServer(server.address)
|
||||
}
|
||||
} else {
|
||||
handleError(`Error refreshing server, refreshing all worlds`)
|
||||
await refreshAllWorlds()
|
||||
@ -349,26 +357,34 @@ const supportsQuickPlay = computed(() =>
|
||||
const filterOptions = computed(() => {
|
||||
const options: FilterBarOption[] = []
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'singleplayer')) {
|
||||
const hasServer = worlds.value.some((x) => x.type === 'server')
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'singleplayer') && hasServer) {
|
||||
options.push({
|
||||
id: 'singleplayer',
|
||||
message: messages.singleplayer,
|
||||
})
|
||||
}
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'server')) {
|
||||
options.push({
|
||||
id: 'server',
|
||||
message: messages.server,
|
||||
})
|
||||
}
|
||||
|
||||
// add available filter if there's any offline ("unavailable") servers
|
||||
if (hasServer) {
|
||||
// add available filter if there's any offline ("unavailable") servers AND there's any singleplayer worlds or available servers
|
||||
if (
|
||||
worlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'server' &&
|
||||
!serverData.value[x.address]?.status &&
|
||||
!serverData.value[x.address]?.refreshing,
|
||||
) &&
|
||||
worlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'singleplayer' ||
|
||||
(x.type === 'server' &&
|
||||
serverData.value[x.address]?.status &&
|
||||
!serverData.value[x.address]?.refreshing),
|
||||
)
|
||||
) {
|
||||
options.push({
|
||||
|
||||
@ -155,7 +155,7 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
@ -170,6 +170,7 @@ import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
|
||||
@ -247,6 +248,9 @@ async function install(version) {
|
||||
installedVersion.value = version
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -23,8 +23,8 @@ export const useInstall = defineStore('installStore', {
|
||||
setInstallConfirmModal(ref) {
|
||||
this.installConfirmModal = ref
|
||||
},
|
||||
showInstallConfirmModal(project, version_id, onInstall) {
|
||||
this.installConfirmModal.show(project, version_id, onInstall)
|
||||
showInstallConfirmModal(project, version_id, onInstall, createInstanceCallback) {
|
||||
this.installConfirmModal.show(project, version_id, onInstall, createInstanceCallback)
|
||||
},
|
||||
setIncompatibilityWarningModal(ref) {
|
||||
this.incompatibilityWarningModal = ref
|
||||
@ -41,7 +41,14 @@ export const useInstall = defineStore('installStore', {
|
||||
},
|
||||
})
|
||||
|
||||
export const install = async (projectId, versionId, instancePath, source, callback = () => {}) => {
|
||||
export const install = async (
|
||||
projectId,
|
||||
versionId,
|
||||
instancePath,
|
||||
source,
|
||||
callback = () => {},
|
||||
createInstanceCallback = () => {},
|
||||
) => {
|
||||
const project = await get_project(projectId, 'must_revalidate').catch(handleError)
|
||||
|
||||
if (project.project_type === 'modpack') {
|
||||
@ -49,7 +56,13 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
||||
const packs = await list().catch(handleError)
|
||||
|
||||
if (packs.length === 0 || !packs.find((pack) => pack.linked_data?.project_id === project.id)) {
|
||||
await packInstall(project.id, version, project.title, project.icon_url).catch(handleError)
|
||||
await packInstall(
|
||||
project.id,
|
||||
version,
|
||||
project.title,
|
||||
project.icon_url,
|
||||
createInstanceCallback,
|
||||
).catch(handleError)
|
||||
|
||||
trackEvent('PackInstall', {
|
||||
id: project.id,
|
||||
@ -61,7 +74,7 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
||||
callback(version)
|
||||
} else {
|
||||
const install = useInstall()
|
||||
install.showInstallConfirmModal(project, version, callback)
|
||||
install.showInstallConfirmModal(project, version, callback, createInstanceCallback)
|
||||
}
|
||||
} else {
|
||||
if (instancePath) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useTheming } from './theme'
|
||||
import { useTheming } from './theme.ts'
|
||||
import { useBreadcrumbs } from './breadcrumbs'
|
||||
import { useLoading } from './loading'
|
||||
import { useNotifications, handleError } from './notifications'
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useTheming = defineStore('themeStore', {
|
||||
state: () => ({
|
||||
themeOptions: ['dark', 'light', 'oled', 'system'],
|
||||
advancedRendering: true,
|
||||
selectedTheme: 'dark',
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: {},
|
||||
}),
|
||||
actions: {
|
||||
setThemeState(newTheme) {
|
||||
if (this.themeOptions.includes(newTheme)) this.selectedTheme = newTheme
|
||||
else console.warn('Selected theme is not present. Check themeOptions.')
|
||||
|
||||
this.setThemeClass()
|
||||
},
|
||||
setThemeClass() {
|
||||
for (const theme of this.themeOptions) {
|
||||
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
||||
}
|
||||
|
||||
let theme = this.selectedTheme
|
||||
if (this.selectedTheme === 'system') {
|
||||
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (darkThemeMq.matches) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(`${theme}-mode`)
|
||||
},
|
||||
},
|
||||
})
|
||||
70
apps/app-frontend/src/store/theme.ts
Normal file
70
apps/app-frontend/src/store/theme.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = {
|
||||
project_background: false,
|
||||
page_path: false,
|
||||
worlds_tab: false,
|
||||
worlds_in_home: true,
|
||||
}
|
||||
|
||||
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
|
||||
|
||||
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
|
||||
export type FeatureFlags = Record<FeatureFlag, boolean>
|
||||
export type ColorTheme = (typeof THEME_OPTIONS)[number]
|
||||
|
||||
export type ThemeStore = {
|
||||
selectedTheme: ColorTheme
|
||||
advancedRendering: boolean
|
||||
toggleSidebar: boolean
|
||||
|
||||
devMode: boolean
|
||||
featureFlags: FeatureFlags
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME_STORE: ThemeStore = {
|
||||
selectedTheme: 'dark',
|
||||
advancedRendering: true,
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: DEFAULT_FEATURE_FLAGS,
|
||||
}
|
||||
|
||||
export const useTheming = defineStore('themeStore', {
|
||||
state: () => DEFAULT_THEME_STORE,
|
||||
actions: {
|
||||
setThemeState(newTheme: ColorTheme) {
|
||||
if (THEME_OPTIONS.includes(newTheme)) {
|
||||
this.selectedTheme = newTheme
|
||||
} else {
|
||||
console.warn('Selected theme is not present. Check themeOptions.')
|
||||
}
|
||||
|
||||
this.setThemeClass()
|
||||
},
|
||||
setThemeClass() {
|
||||
for (const theme of THEME_OPTIONS) {
|
||||
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
||||
}
|
||||
|
||||
let theme = this.selectedTheme
|
||||
if (this.selectedTheme === 'system') {
|
||||
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (darkThemeMq.matches) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(`${theme}-mode`)
|
||||
},
|
||||
getFeatureFlag(key: FeatureFlag) {
|
||||
return this.featureFlags[key] ?? DEFAULT_FEATURE_FLAGS[key]
|
||||
},
|
||||
getThemeOptions() {
|
||||
return THEME_OPTIONS
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -9,5 +9,6 @@ edition = "2021"
|
||||
theseus = { path = "../../packages/app-lib", features = ["cli"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
webbrowser = "0.8.13"
|
||||
enumset = "1.1"
|
||||
|
||||
tracing = "0.1.37"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use enumset::EnumSet;
|
||||
use theseus::prelude::*;
|
||||
use theseus::worlds::get_recent_worlds;
|
||||
|
||||
@ -40,7 +41,7 @@ async fn main() -> theseus::Result<()> {
|
||||
// Initialize state
|
||||
State::init().await?;
|
||||
|
||||
let worlds = get_recent_worlds(4).await?;
|
||||
let worlds = get_recent_worlds(4, EnumSet::all()).await?;
|
||||
for world in worlds {
|
||||
println!(
|
||||
"World: {:?}/{:?} played at {:?}: {:#?}",
|
||||
|
||||
@ -42,6 +42,7 @@ tracing-error = "0.2.0"
|
||||
|
||||
dashmap = "6.0.1"
|
||||
paste = "1.0.15"
|
||||
enumset = { version = "1.1", features = ["serde"] }
|
||||
|
||||
opener = { version = "0.7.2", features = ["reveal", "dbus-vendored"] }
|
||||
|
||||
|
||||
@ -248,6 +248,7 @@ fn main() {
|
||||
"get_recent_worlds",
|
||||
"get_profile_worlds",
|
||||
"get_singleplayer_world",
|
||||
"set_world_display_status",
|
||||
"rename_world",
|
||||
"reset_world_icon",
|
||||
"backup_world",
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
use crate::api::Result;
|
||||
use either::Either;
|
||||
use enumset::EnumSet;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use theseus::prelude::ProcessMetadata;
|
||||
use theseus::profile::{get_full_path, QuickPlayType};
|
||||
use theseus::worlds::{
|
||||
ServerPackStatus, ServerStatus, World, WorldWithProfile,
|
||||
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
||||
WorldWithProfile,
|
||||
};
|
||||
use theseus::{profile, worlds};
|
||||
|
||||
@ -14,6 +16,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
get_recent_worlds,
|
||||
get_profile_worlds,
|
||||
get_singleplayer_world,
|
||||
set_world_display_status,
|
||||
rename_world,
|
||||
reset_world_icon,
|
||||
backup_world,
|
||||
@ -33,8 +36,13 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
pub async fn get_recent_worlds<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
limit: usize,
|
||||
display_statuses: Option<EnumSet<DisplayStatus>>,
|
||||
) -> Result<Vec<WorldWithProfile>> {
|
||||
let mut result = worlds::get_recent_worlds(limit).await?;
|
||||
let mut result = worlds::get_recent_worlds(
|
||||
limit,
|
||||
display_statuses.unwrap_or(EnumSet::all()),
|
||||
)
|
||||
.await?;
|
||||
for world in result.iter_mut() {
|
||||
adapt_world_icon(&app_handle, &mut world.world);
|
||||
}
|
||||
@ -59,8 +67,7 @@ pub async fn get_singleplayer_world<R: Runtime>(
|
||||
instance: &str,
|
||||
world: &str,
|
||||
) -> Result<World> {
|
||||
let instance = get_full_path(instance).await?;
|
||||
let mut world = worlds::get_singleplayer_world(&instance, world).await?;
|
||||
let mut world = worlds::get_singleplayer_world(instance, world).await?;
|
||||
adapt_world_icon(&app_handle, &mut world);
|
||||
Ok(world)
|
||||
}
|
||||
@ -90,6 +97,22 @@ fn adapt_world_icon<R: Runtime>(app_handle: &AppHandle<R>, world: &mut World) {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_world_display_status(
|
||||
instance: &str,
|
||||
world_type: WorldType,
|
||||
world_id: &str,
|
||||
display_status: DisplayStatus,
|
||||
) -> Result<()> {
|
||||
Ok(worlds::set_world_display_status(
|
||||
instance,
|
||||
world_type,
|
||||
world_id,
|
||||
display_status,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rename_world(
|
||||
instance: &str,
|
||||
|
||||
@ -117,8 +117,7 @@ fn show_window(app: tauri::AppHandle) {
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text(&format!(
|
||||
"Cannot display application window due to an error:\n{}",
|
||||
e
|
||||
"Cannot display application window due to an error:\n{e}"
|
||||
))
|
||||
.show_alert()
|
||||
.unwrap();
|
||||
@ -138,8 +137,7 @@ fn is_dev() -> bool {
|
||||
async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
|
||||
window.set_decorations(b).map_err(|e| {
|
||||
theseus::Error::from(theseus::ErrorKind::OtherError(format!(
|
||||
"Failed to toggle decorations: {}",
|
||||
e
|
||||
"Failed to toggle decorations: {e}"
|
||||
)))
|
||||
})?;
|
||||
Ok(())
|
||||
@ -320,7 +318,7 @@ fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
|
||||
if format!("{:?}", e).contains(
|
||||
if format!("{e:?}").contains(
|
||||
"Runtime(CreateWebview(WebView2Error(WindowsError",
|
||||
) {
|
||||
MessageDialog::new()
|
||||
@ -338,8 +336,7 @@ fn main() {
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text(&format!(
|
||||
"Cannot initialize application due to an error:\n{:?}",
|
||||
e
|
||||
"Cannot initialize application due to an error:\n{e:?}"
|
||||
))
|
||||
.show_alert()
|
||||
.unwrap();
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/inter/"],
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"script-src": "https://*.posthog.com 'self'",
|
||||
|
||||
@ -57,7 +57,7 @@ pub async fn fetch_forge(
|
||||
|
||||
ForgeVersion {
|
||||
format_version,
|
||||
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{loader_version}/forge-{loader_version}-installer.jar"),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: game_version.clone(),
|
||||
@ -137,7 +137,7 @@ pub async fn fetch_neo(
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{loader_version}/forge-{loader_version}-installer.jar"),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: "1.20.1".to_string(), // All NeoForge Forge versions are for 1.20.1
|
||||
@ -163,7 +163,7 @@ pub async fn fetch_neo(
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{0}/neoforge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{loader_version}/neoforge-{loader_version}-installer.jar"),
|
||||
loader_version: loader_version.clone(),
|
||||
raw: loader_version,
|
||||
game_version,
|
||||
@ -502,7 +502,7 @@ async fn fetch(
|
||||
)?;
|
||||
|
||||
artifact.url =
|
||||
format_url(&format!("maven/{}", artifact_path));
|
||||
format_url(&format!("maven/{artifact_path}"));
|
||||
|
||||
return Ok(lib);
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ impl actix_web::ResponseError for OAuthError {
|
||||
);
|
||||
|
||||
if let Some(state) = self.state.as_ref() {
|
||||
redirect_uri = format!("{}&state={}", redirect_uri, state);
|
||||
redirect_uri = format!("{redirect_uri}&state={state}");
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
|
||||
@ -414,7 +414,7 @@ fn generate_access_token() -> String {
|
||||
.take(60)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
format!("mro_{}", random)
|
||||
format!("mro_{random}")
|
||||
}
|
||||
|
||||
async fn init_oauth_code_flow(
|
||||
|
||||
@ -32,7 +32,7 @@ impl Display for ErrorPage {
|
||||
let html = include_str!("error.html")
|
||||
.replace("{{ code }}", &self.code.to_string())
|
||||
.replace("{{ message }}", &self.message);
|
||||
write!(f, "{}", html)?;
|
||||
write!(f, "{html}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -103,8 +103,7 @@ impl MinecraftGameVersion {
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Game version requires field value to be an enum: {:?}",
|
||||
version_field
|
||||
"Game version requires field value to be an enum: {version_field:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@ -1080,8 +1080,7 @@ impl VersionFieldValue {
|
||||
let field_name = field_type.to_str();
|
||||
let did_not_exist_error = |field_name: &str, desired_field: &str| {
|
||||
DatabaseError::SchemaError(format!(
|
||||
"Field name {} for field {} in does not exist",
|
||||
desired_field, field_name
|
||||
"Field name {desired_field} for field {field_name} in does not exist"
|
||||
))
|
||||
};
|
||||
|
||||
@ -1103,8 +1102,7 @@ impl VersionFieldValue {
|
||||
.collect::<Vec<_>>();
|
||||
if field_id.len() > 1 {
|
||||
return Err(DatabaseError::SchemaError(format!(
|
||||
"Multiple field ids for field {}",
|
||||
field_name
|
||||
"Multiple field ids for field {field_name}"
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@ -912,7 +912,7 @@ impl Version {
|
||||
file.hashes.iter().map(|(algo, hash)| {
|
||||
(
|
||||
VERSION_FILES_NAMESPACE,
|
||||
Some(format!("{}_{}", algo, hash)),
|
||||
Some(format!("{algo}_{hash}")),
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
@ -80,10 +80,9 @@ impl From<DBNotification> for Notification {
|
||||
} => (
|
||||
"A project you follow has been updated!".to_string(),
|
||||
format!(
|
||||
"The project {} has released a new version: {}",
|
||||
project_id, version_id
|
||||
"The project {project_id} has released a new version: {version_id}"
|
||||
),
|
||||
format!("/project/{}/version/{}", project_id, version_id),
|
||||
format!("/project/{project_id}/version/{version_id}"),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::TeamInvite {
|
||||
@ -93,8 +92,8 @@ impl From<DBNotification> for Notification {
|
||||
..
|
||||
} => (
|
||||
"You have been invited to join a team!".to_string(),
|
||||
format!("An invite has been sent for you to be {} of a team", role),
|
||||
format!("/project/{}", project_id),
|
||||
format!("An invite has been sent for you to be {role} of a team"),
|
||||
format!("/project/{project_id}"),
|
||||
vec![
|
||||
NotificationAction {
|
||||
name: "Accept".to_string(),
|
||||
@ -117,10 +116,9 @@ impl From<DBNotification> for Notification {
|
||||
} => (
|
||||
"You have been invited to join an organization!".to_string(),
|
||||
format!(
|
||||
"An invite has been sent for you to be {} of an organization",
|
||||
role
|
||||
"An invite has been sent for you to be {role} of an organization"
|
||||
),
|
||||
format!("/organization/{}", organization_id),
|
||||
format!("/organization/{organization_id}"),
|
||||
vec![
|
||||
NotificationAction {
|
||||
name: "Accept".to_string(),
|
||||
@ -149,7 +147,7 @@ impl From<DBNotification> for Notification {
|
||||
old_status.as_friendly_str(),
|
||||
new_status.as_friendly_str()
|
||||
),
|
||||
format!("/project/{}", project_id),
|
||||
format!("/project/{project_id}"),
|
||||
vec![],
|
||||
),
|
||||
NotificationBody::ModeratorMessage {
|
||||
@ -160,9 +158,9 @@ impl From<DBNotification> for Notification {
|
||||
"A moderator has sent you a message!".to_string(),
|
||||
"Click on the link to read more.".to_string(),
|
||||
if let Some(project_id) = project_id {
|
||||
format!("/project/{}", project_id)
|
||||
format!("/project/{project_id}")
|
||||
} else if let Some(report_id) = report_id {
|
||||
format!("/project/{}", report_id)
|
||||
format!("/project/{report_id}")
|
||||
} else {
|
||||
"#".to_string()
|
||||
},
|
||||
|
||||
@ -222,8 +222,7 @@ pub async fn delphi_result_ingest(
|
||||
for (issue, trace) in &body.issues {
|
||||
for (path, code) in trace {
|
||||
header.push_str(&format!(
|
||||
"\n issue {issue} found at file {}: \n ```\n{}\n```",
|
||||
path, code
|
||||
"\n issue {issue} found at file {path}: \n ```\n{code}\n```"
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -242,10 +241,8 @@ pub async fn delphi_result_ingest(
|
||||
|
||||
for (issue, trace) in &body.issues {
|
||||
for path in trace.keys() {
|
||||
thread_header.push_str(&format!(
|
||||
"\n\n- issue {issue} found at file {}",
|
||||
path
|
||||
));
|
||||
thread_header
|
||||
.push_str(&format!("\n\n- issue {issue} found at file {path}"));
|
||||
}
|
||||
|
||||
if trace.is_empty() {
|
||||
|
||||
@ -247,7 +247,7 @@ impl AuthProvider {
|
||||
state: String,
|
||||
) -> Result<String, AuthenticationError> {
|
||||
let self_addr = dotenvy::var("SELF_ADDR")?;
|
||||
let raw_redirect_uri = format!("{}/v2/auth/callback", self_addr);
|
||||
let raw_redirect_uri = format!("{self_addr}/v2/auth/callback");
|
||||
let redirect_uri = urlencoding::encode(&raw_redirect_uri);
|
||||
|
||||
Ok(match self {
|
||||
@ -255,30 +255,24 @@ impl AuthProvider {
|
||||
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://github.com/login/oauth/authorize?client_id={}&prompt=select_account&state={}&scope=read%3Auser%20user%3Aemail&redirect_uri={}",
|
||||
client_id,
|
||||
state,
|
||||
redirect_uri,
|
||||
"https://github.com/login/oauth/authorize?client_id={client_id}&prompt=select_account&state={state}&scope=read%3Auser%20user%3Aemail&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Discord => {
|
||||
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?;
|
||||
|
||||
format!("https://discord.com/api/oauth2/authorize?client_id={}&state={}&response_type=code&scope=identify%20email&redirect_uri={}", client_id, state, redirect_uri)
|
||||
format!("https://discord.com/api/oauth2/authorize?client_id={client_id}&state={state}&response_type=code&scope=identify%20email&redirect_uri={redirect_uri}")
|
||||
}
|
||||
AuthProvider::Microsoft => {
|
||||
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?;
|
||||
|
||||
format!("https://login.live.com/oauth20_authorize.srf?client_id={}&response_type=code&scope=user.read&state={}&prompt=select_account&redirect_uri={}", client_id, state, redirect_uri)
|
||||
format!("https://login.live.com/oauth20_authorize.srf?client_id={client_id}&response_type=code&scope=user.read&state={state}&prompt=select_account&redirect_uri={redirect_uri}")
|
||||
}
|
||||
AuthProvider::GitLab => {
|
||||
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?;
|
||||
|
||||
format!(
|
||||
"https://gitlab.com/oauth/authorize?client_id={}&state={}&scope=read_user+profile+email&response_type=code&redirect_uri={}",
|
||||
client_id,
|
||||
state,
|
||||
redirect_uri,
|
||||
"https://gitlab.com/oauth/authorize?client_id={client_id}&state={state}&scope=read_user+profile+email&response_type=code&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Google => {
|
||||
@ -342,8 +336,7 @@ impl AuthProvider {
|
||||
let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?;
|
||||
|
||||
let url = format!(
|
||||
"https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}&redirect_uri={}",
|
||||
client_id, client_secret, code, redirect_uri
|
||||
"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}"
|
||||
);
|
||||
|
||||
let token: AccessToken = reqwest::Client::new()
|
||||
@ -482,9 +475,8 @@ impl AuthProvider {
|
||||
form.insert("openid.mode".to_string(), "check_authentication");
|
||||
|
||||
for val in signed.split(',') {
|
||||
if let Some(arr_val) = query.get(&format!("openid.{}", val))
|
||||
{
|
||||
form.insert(format!("openid.{}", val), &**arr_val);
|
||||
if let Some(arr_val) = query.get(&format!("openid.{val}")) {
|
||||
form.insert(format!("openid.{val}"), &**arr_val);
|
||||
}
|
||||
}
|
||||
|
||||
@ -621,8 +613,7 @@ impl AuthProvider {
|
||||
email: discord_user.email,
|
||||
avatar_url: discord_user.avatar.map(|x| {
|
||||
format!(
|
||||
"https://cdn.discordapp.com/avatars/{}/{}.webp",
|
||||
id, x
|
||||
"https://cdn.discordapp.com/avatars/{id}/{x}.webp"
|
||||
)
|
||||
}),
|
||||
bio: None,
|
||||
@ -741,9 +732,7 @@ impl AuthProvider {
|
||||
|
||||
let response: String = reqwest::get(
|
||||
&format!(
|
||||
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={}&steamids={}",
|
||||
api_key,
|
||||
token
|
||||
"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={api_key}&steamids={token}"
|
||||
)
|
||||
)
|
||||
.await?
|
||||
@ -1367,7 +1356,7 @@ pub async fn create_account_with_password(
|
||||
if let Some(feedback) =
|
||||
score.feedback().clone().and_then(|x| x.warning())
|
||||
{
|
||||
format!("Password too weak: {}", feedback)
|
||||
format!("Password too weak: {feedback}")
|
||||
} else {
|
||||
"Specified password is too weak! Please improve its strength."
|
||||
.to_string()
|
||||
@ -2030,7 +2019,7 @@ pub async fn change_password(
|
||||
if let Some(feedback) =
|
||||
score.feedback().clone().and_then(|x| x.warning())
|
||||
{
|
||||
format!("Password too weak: {}", feedback)
|
||||
format!("Password too weak: {feedback}")
|
||||
} else {
|
||||
"Specified password is too weak! Please improve its strength.".to_string()
|
||||
},
|
||||
@ -2085,8 +2074,8 @@ pub async fn change_password(
|
||||
|
||||
send_email(
|
||||
email,
|
||||
&format!("Password {}", changed),
|
||||
&format!("Your password has been {} on your account.", changed),
|
||||
&format!("Password {changed}"),
|
||||
&format!("Your password has been {changed} on your account."),
|
||||
"If you did not make this change, please contact us immediately through our support channels on Discord or via email (support@modrinth.com).",
|
||||
None,
|
||||
)?;
|
||||
|
||||
@ -113,7 +113,7 @@ pub async fn create_pat(
|
||||
.take(60)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
let token = format!("mrp_{}", token);
|
||||
let token = format!("mrp_{token}");
|
||||
|
||||
let name = info.name.clone();
|
||||
database::models::pat_item::PersonalAccessToken {
|
||||
|
||||
@ -116,7 +116,7 @@ pub async fn forge_updates(
|
||||
for game_version in &game_versions {
|
||||
response
|
||||
.promos
|
||||
.entry(format!("{}-recommended", game_version))
|
||||
.entry(format!("{game_version}-recommended"))
|
||||
.or_insert_with(|| version.version_number.clone());
|
||||
}
|
||||
}
|
||||
@ -124,7 +124,7 @@ pub async fn forge_updates(
|
||||
for game_version in &game_versions {
|
||||
response
|
||||
.promos
|
||||
.entry(format!("{}-latest", game_version))
|
||||
.entry(format!("{game_version}-latest"))
|
||||
.or_insert_with(|| version.version_number.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ where
|
||||
|
||||
match (
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={}", boundary).as_str(),
|
||||
format!("multipart/form-data; boundary={boundary}").as_str(),
|
||||
)
|
||||
.try_into_pair()
|
||||
{
|
||||
@ -153,8 +153,7 @@ where
|
||||
}
|
||||
Err(err) => {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Error inserting test header: {:?}.",
|
||||
err
|
||||
"Error inserting test header: {err:?}."
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@ -424,7 +424,7 @@ pub async fn collection_icon_edit(
|
||||
|
||||
let collection_id: CollectionId = collection_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", collection_id),
|
||||
&format!("data/{collection_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@ -393,7 +393,7 @@ pub async fn oauth_client_icon_edit(
|
||||
)
|
||||
.await?;
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", client_id),
|
||||
&format!("data/{client_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@ -1096,7 +1096,7 @@ pub async fn organization_icon_edit(
|
||||
|
||||
let organization_id: OrganizationId = organization_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", organization_id),
|
||||
&format!("data/{organization_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@ -676,8 +676,7 @@ pub async fn cancel_payout(
|
||||
.make_paypal_request::<(), ()>(
|
||||
Method::POST,
|
||||
&format!(
|
||||
"payments/payouts-item/{}/cancel",
|
||||
platform_id
|
||||
"payments/payouts-item/{platform_id}/cancel"
|
||||
),
|
||||
None,
|
||||
None,
|
||||
@ -689,7 +688,7 @@ pub async fn cancel_payout(
|
||||
payouts
|
||||
.make_tremendous_request::<(), ()>(
|
||||
Method::POST,
|
||||
&format!("rewards/{}/cancel", platform_id),
|
||||
&format!("rewards/{platform_id}/cancel"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -810,8 +810,7 @@ async fn project_create_inner(
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'project' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'project' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@ -830,8 +829,7 @@ async fn project_create_inner(
|
||||
image_item::Image::clear_cache(image.id.into(), redis).await?;
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} does not exist",
|
||||
image_id
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -710,10 +710,8 @@ pub async fn project_edit(
|
||||
));
|
||||
}
|
||||
|
||||
let ids_to_delete = links
|
||||
.iter()
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
let ids_to_delete =
|
||||
links.keys().cloned().collect::<Vec<String>>();
|
||||
// Deletes all links from hashmap- either will be deleted or be replaced
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -1270,10 +1268,7 @@ pub async fn projects_edit(
|
||||
.await?;
|
||||
|
||||
if let Some(links) = &bulk_edit_project.link_urls {
|
||||
let ids_to_delete = links
|
||||
.iter()
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
let ids_to_delete = links.keys().cloned().collect::<Vec<String>>();
|
||||
// Deletes all links from hashmap- either will be deleted or be replaced
|
||||
sqlx::query!(
|
||||
"
|
||||
@ -1482,7 +1477,7 @@ pub async fn project_icon_edit(
|
||||
|
||||
let project_id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", project_id),
|
||||
&format!("data/{project_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
@ -1700,7 +1695,7 @@ pub async fn add_gallery_item(
|
||||
|
||||
let id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}/images", id),
|
||||
&format!("data/{id}/images"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(350),
|
||||
|
||||
@ -178,8 +178,7 @@ pub async fn report_create(
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'report' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'report' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@ -198,8 +197,7 @@ pub async fn report_create(
|
||||
image_item::Image::clear_cache(image.id.into(), &redis).await?;
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} could not be found",
|
||||
image_id
|
||||
"Image {image_id} could not be found"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -527,8 +527,7 @@ pub async fn thread_send_message(
|
||||
) || image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'thread_message' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'thread_message' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@ -548,8 +547,7 @@ pub async fn thread_send_message(
|
||||
.await?;
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
"Image {} does not exist",
|
||||
image_id
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -595,7 +595,7 @@ pub async fn user_icon_edit(
|
||||
|
||||
let user_id: UserId = actual_user.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", user_id),
|
||||
&format!("data/{user_id}"),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
|
||||
@ -486,8 +486,7 @@ async fn version_create_inner(
|
||||
|| image.context.inner_id().is_some()
|
||||
{
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} is not unused and in the 'version' context",
|
||||
image_id
|
||||
"Image {image_id} is not unused and in the 'version' context"
|
||||
)));
|
||||
}
|
||||
|
||||
@ -506,8 +505,7 @@ async fn version_create_inner(
|
||||
image_item::Image::clear_cache(image.id.into(), redis).await?;
|
||||
} else {
|
||||
return Err(CreateError::InvalidInput(format!(
|
||||
"Image {} does not exist",
|
||||
image_id
|
||||
"Image {image_id} does not exist"
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -810,7 +808,7 @@ pub async fn upload_file(
|
||||
) -> Result<(), CreateError> {
|
||||
let (file_name, file_extension) = get_name_ext(content_disposition)?;
|
||||
|
||||
if other_file_names.contains(&format!("{}.{}", file_name, file_extension)) {
|
||||
if other_file_names.contains(&format!("{file_name}.{file_extension}")) {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"Duplicate files are not allowed to be uploaded to Modrinth!"
|
||||
.to_string(),
|
||||
|
||||
@ -67,5 +67,5 @@ pub async fn push_back_user_expiry(
|
||||
}
|
||||
|
||||
fn get_field_name(user: UserId) -> String {
|
||||
format!("user_status:{}", user)
|
||||
format!("user_status:{user}")
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ impl AppendsMultipart for TestRequest {
|
||||
let (boundary, payload) = generate_multipart(data);
|
||||
self.append_header((
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={}", boundary),
|
||||
format!("multipart/form-data; boundary={boundary}"),
|
||||
))
|
||||
.set_payload(payload)
|
||||
}
|
||||
@ -62,17 +62,12 @@ pub fn generate_multipart(
|
||||
|
||||
if let Some(filename) = &segment.filename {
|
||||
payload.extend_from_slice(
|
||||
format!("; filename=\"{filename}\"", filename = filename)
|
||||
.as_bytes(),
|
||||
format!("; filename=\"{filename}\"").as_bytes(),
|
||||
);
|
||||
}
|
||||
if let Some(content_type) = &segment.content_type {
|
||||
payload.extend_from_slice(
|
||||
format!(
|
||||
"\r\nContent-Type: {content_type}",
|
||||
content_type = content_type
|
||||
)
|
||||
.as_bytes(),
|
||||
format!("\r\nContent-Type: {content_type}").as_bytes(),
|
||||
);
|
||||
}
|
||||
payload.extend_from_slice(b"\r\n\r\n");
|
||||
@ -87,9 +82,7 @@ pub fn generate_multipart(
|
||||
}
|
||||
payload.extend_from_slice(b"\r\n");
|
||||
}
|
||||
payload.extend_from_slice(
|
||||
format!("--{boundary}--\r\n", boundary = boundary).as_bytes(),
|
||||
);
|
||||
payload.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
|
||||
|
||||
(boundary, Bytes::from(payload))
|
||||
}
|
||||
|
||||
@ -51,8 +51,7 @@ pub async fn upload_image_optimized(
|
||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Invalid format for image: {}",
|
||||
file_extension
|
||||
"Invalid format for image: {file_extension}"
|
||||
))
|
||||
})?;
|
||||
|
||||
@ -91,7 +90,7 @@ pub async fn upload_image_optimized(
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("{}/{}.{}", upload_folder, hash, file_extension),
|
||||
&format!("{upload_folder}/{hash}.{file_extension}"),
|
||||
bytes,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -71,7 +71,7 @@ impl ApiV2 {
|
||||
};
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/search?{}{}", query_field, facets_field))
|
||||
.uri(&format!("/v2/search?{query_field}{facets_field}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@ -99,7 +99,7 @@ impl ApiProject for ApiV2 {
|
||||
|
||||
// Approve as a moderator.
|
||||
let req = TestRequest::patch()
|
||||
.uri(&format!("/v2/project/{}", slug))
|
||||
.uri(&format!("/v2/project/{slug}"))
|
||||
.append_pat(MOD_USER_PAT)
|
||||
.set_json(json!(
|
||||
{
|
||||
@ -114,7 +114,7 @@ impl ApiProject for ApiV2 {
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v2/project/{}/version", slug))
|
||||
.uri(&format!("/v2/project/{slug}/version"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@ -217,7 +217,7 @@ impl ApiProject for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/user/{}/projects", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}/projects"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@ -260,7 +260,7 @@ impl ApiProject for ApiV2 {
|
||||
) -> ServiceResponse {
|
||||
let projects_str = ids_or_slugs
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.map(|s| format!("\"{s}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let req = test::TestRequest::patch()
|
||||
@ -490,13 +490,13 @@ impl ApiProject for ApiV2 {
|
||||
featured = featured
|
||||
);
|
||||
if let Some(title) = title {
|
||||
url.push_str(&format!("&title={}", title));
|
||||
url.push_str(&format!("&title={title}"));
|
||||
}
|
||||
if let Some(description) = description {
|
||||
url.push_str(&format!("&description={}", description));
|
||||
url.push_str(&format!("&description={description}"));
|
||||
}
|
||||
if let Some(ordering) = ordering {
|
||||
url.push_str(&format!("&ordering={}", ordering));
|
||||
url.push_str(&format!("&ordering={ordering}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
@ -542,10 +542,7 @@ impl ApiProject for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v2/project/{id_or_slug}/gallery?url={url}",
|
||||
url = url
|
||||
))
|
||||
.uri(&format!("/v2/project/{id_or_slug}/gallery?url={url}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ impl ApiUser for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@ -32,7 +32,7 @@ impl ApiUser for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
@ -46,7 +46,7 @@ impl ApiUser for ApiV2 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v2/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
|
||||
@ -399,18 +399,18 @@ impl ApiVersion for ApiV2 {
|
||||
));
|
||||
}
|
||||
if let Some(featured) = featured {
|
||||
query_string.push_str(&format!("&featured={}", featured));
|
||||
query_string.push_str(&format!("&featured={featured}"));
|
||||
}
|
||||
if let Some(version_type) = version_type {
|
||||
query_string.push_str(&format!("&version_type={}", version_type));
|
||||
query_string.push_str(&format!("&version_type={version_type}"));
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
let limit = limit.to_string();
|
||||
query_string.push_str(&format!("&limit={}", limit));
|
||||
query_string.push_str(&format!("&limit={limit}"));
|
||||
}
|
||||
if let Some(offset) = offset {
|
||||
let offset = offset.to_string();
|
||||
query_string.push_str(&format!("&offset={}", offset));
|
||||
query_string.push_str(&format!("&offset={offset}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
@ -480,7 +480,7 @@ impl ApiVersion for ApiV2 {
|
||||
) -> ServiceResponse {
|
||||
let ids = url_encode_json_serialized_vec(&version_ids);
|
||||
let request = test::TestRequest::get()
|
||||
.uri(&format!("/v2/versions?ids={}", ids))
|
||||
.uri(&format!("/v2/versions?ids={ids}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
|
||||
@ -157,7 +157,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/collections", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}/collections"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
|
||||
@ -104,8 +104,7 @@ impl ApiV3 {
|
||||
code: auth_code,
|
||||
redirect_uri: original_redirect_uri,
|
||||
client_id: serde_json::from_str(&format!(
|
||||
"\"{}\"",
|
||||
client_id
|
||||
"\"{client_id}\""
|
||||
))
|
||||
.unwrap(),
|
||||
})
|
||||
|
||||
@ -47,7 +47,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> Vec<OAuthClient> {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/oauth_apps", user_id))
|
||||
.uri(&format!("/v3/user/{user_id}/oauth_apps"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@ -62,7 +62,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/_internal/oauth/app/{}", client_id))
|
||||
.uri(&format!("/_internal/oauth/app/{client_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
@ -93,7 +93,7 @@ impl ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::delete()
|
||||
.uri(&format!("/_internal/oauth/app/{}", client_id))
|
||||
.uri(&format!("/_internal/oauth/app/{client_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ impl ApiProject for ApiV3 {
|
||||
|
||||
// Approve as a moderator.
|
||||
let req = TestRequest::patch()
|
||||
.uri(&format!("/v3/project/{}", slug))
|
||||
.uri(&format!("/v3/project/{slug}"))
|
||||
.append_pat(MOD_USER_PAT)
|
||||
.set_json(json!(
|
||||
{
|
||||
@ -69,7 +69,7 @@ impl ApiProject for ApiV3 {
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/project/{}/version", slug))
|
||||
.uri(&format!("/v3/project/{slug}/version"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@ -172,7 +172,7 @@ impl ApiProject for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/projects", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}/projects"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@ -215,7 +215,7 @@ impl ApiProject for ApiV3 {
|
||||
) -> ServiceResponse {
|
||||
let projects_str = ids_or_slugs
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.map(|s| format!("\"{s}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let req = test::TestRequest::patch()
|
||||
@ -363,13 +363,13 @@ impl ApiProject for ApiV3 {
|
||||
featured = featured
|
||||
);
|
||||
if let Some(title) = title {
|
||||
url.push_str(&format!("&title={}", title));
|
||||
url.push_str(&format!("&title={title}"));
|
||||
}
|
||||
if let Some(description) = description {
|
||||
url.push_str(&format!("&description={}", description));
|
||||
url.push_str(&format!("&description={description}"));
|
||||
}
|
||||
if let Some(ordering) = ordering {
|
||||
url.push_str(&format!("&ordering={}", ordering));
|
||||
url.push_str(&format!("&ordering={ordering}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
@ -416,10 +416,7 @@ impl ApiProject for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/project/{id_or_slug}/gallery?url={url}",
|
||||
url = url
|
||||
))
|
||||
.uri(&format!("/v3/project/{id_or_slug}/gallery?url={url}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
@ -562,7 +559,7 @@ impl ApiV3 {
|
||||
};
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/search?{}{}", query_field, facets_field))
|
||||
.uri(&format!("/v3/search?{query_field}{facets_field}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
@ -583,12 +580,12 @@ impl ApiV3 {
|
||||
let version_string: String =
|
||||
serde_json::to_string(&id_or_slugs).unwrap();
|
||||
let version_string = urlencoding::encode(&version_string);
|
||||
format!("version_ids={}", version_string)
|
||||
format!("version_ids={version_string}")
|
||||
} else {
|
||||
let projects_string: String =
|
||||
serde_json::to_string(&id_or_slugs).unwrap();
|
||||
let projects_string = urlencoding::encode(&projects_string);
|
||||
format!("project_ids={}", projects_string)
|
||||
format!("project_ids={projects_string}")
|
||||
};
|
||||
|
||||
let mut extra_args = String::new();
|
||||
@ -605,10 +602,8 @@ impl ApiV3 {
|
||||
extra_args.push_str(&format!("&end_date={end_date}"));
|
||||
}
|
||||
if let Some(resolution_minutes) = resolution_minutes {
|
||||
extra_args.push_str(&format!(
|
||||
"&resolution_minutes={}",
|
||||
resolution_minutes
|
||||
));
|
||||
extra_args
|
||||
.push_str(&format!("&resolution_minutes={resolution_minutes}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
|
||||
@ -76,7 +76,7 @@ impl ApiV3 {
|
||||
loader_field: &str,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/loader_field?loader_field={}", loader_field))
|
||||
.uri(&format!("/v3/loader_field?loader_field={loader_field}"))
|
||||
.append_pat(ADMIN_USER_PAT)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
|
||||
@ -13,7 +13,7 @@ impl ApiUser for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
@ -34,7 +34,7 @@ impl ApiUser for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
@ -48,7 +48,7 @@ impl ApiUser for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.uri(&format!("/v3/user/{user_id_or_username}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
|
||||
@ -432,18 +432,18 @@ impl ApiVersion for ApiV3 {
|
||||
));
|
||||
}
|
||||
if let Some(featured) = featured {
|
||||
query_string.push_str(&format!("&featured={}", featured));
|
||||
query_string.push_str(&format!("&featured={featured}"));
|
||||
}
|
||||
if let Some(version_type) = version_type {
|
||||
query_string.push_str(&format!("&version_type={}", version_type));
|
||||
query_string.push_str(&format!("&version_type={version_type}"));
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
let limit = limit.to_string();
|
||||
query_string.push_str(&format!("&limit={}", limit));
|
||||
query_string.push_str(&format!("&limit={limit}"));
|
||||
}
|
||||
if let Some(offset) = offset {
|
||||
let offset = offset.to_string();
|
||||
query_string.push_str(&format!("&offset={}", offset));
|
||||
query_string.push_str(&format!("&offset={offset}"));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
@ -513,7 +513,7 @@ impl ApiVersion for ApiV3 {
|
||||
) -> ServiceResponse {
|
||||
let ids = url_encode_json_serialized_vec(&version_ids);
|
||||
let request = test::TestRequest::get()
|
||||
.uri(&format!("/v3/versions?ids={}", ids))
|
||||
.uri(&format!("/v3/versions?ids={ids}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
@ -546,10 +546,7 @@ impl ApiVersion for ApiV3 {
|
||||
Some(file),
|
||||
);
|
||||
let request = test::TestRequest::post()
|
||||
.uri(&format!(
|
||||
"/v3/version/{version_id}/file",
|
||||
version_id = version_id
|
||||
))
|
||||
.uri(&format!("/v3/version/{version_id}/file"))
|
||||
.append_pat(pat)
|
||||
.set_multipart(m)
|
||||
.to_request();
|
||||
@ -562,10 +559,7 @@ impl ApiVersion for ApiV3 {
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let request = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/version/{version_id}",
|
||||
version_id = version_id
|
||||
))
|
||||
.uri(&format!("/v3/version/{version_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
|
||||
@ -137,7 +137,7 @@ impl TemporaryDatabase {
|
||||
dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
let mut template_url =
|
||||
Url::parse(&url).expect("Invalid database URL");
|
||||
template_url.set_path(&format!("/{}", TEMPLATE_DATABASE_NAME));
|
||||
template_url.set_path(&format!("/{TEMPLATE_DATABASE_NAME}"));
|
||||
|
||||
let pool = PgPool::connect(template_url.as_str())
|
||||
.await
|
||||
|
||||
@ -470,8 +470,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != ProjectPermissions::empty() {
|
||||
return Err(format!(
|
||||
"Test 1 failed. Expected no permissions, got {:?}",
|
||||
p
|
||||
"Test 1 failed. Expected no permissions, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -511,8 +510,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != ProjectPermissions::empty() {
|
||||
return Err(format!(
|
||||
"Test 2 failed. Expected no permissions, got {:?}",
|
||||
p
|
||||
"Test 2 failed. Expected no permissions, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -561,8 +559,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_project_permissions {
|
||||
return Err(format!(
|
||||
"Test 3 failed. Expected {:?}, got {:?}",
|
||||
failure_project_permissions, p
|
||||
"Test 3 failed. Expected {failure_project_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -607,8 +604,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 4 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 4 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -666,8 +662,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_project_permissions {
|
||||
return Err(format!(
|
||||
"Test 5 failed. Expected {:?}, got {:?}",
|
||||
failure_project_permissions, p
|
||||
"Test 5 failed. Expected {failure_project_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -721,8 +716,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 6 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 6 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -790,8 +784,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_project_permissions {
|
||||
return Err(format!(
|
||||
"Test 7 failed. Expected {:?}, got {:?}",
|
||||
failure_project_permissions, p
|
||||
"Test 7 failed. Expected {failure_project_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -856,8 +849,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 8 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 8 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
|
||||
@ -927,8 +919,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != OrganizationPermissions::empty() {
|
||||
return Err(format!(
|
||||
"Test 1 failed. Expected no permissions, got {:?}",
|
||||
p
|
||||
"Test 1 failed. Expected no permissions, got {p:?}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
@ -976,8 +967,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != failure_organization_permissions {
|
||||
return Err(format!(
|
||||
"Test 2 failed. Expected {:?}, got {:?}",
|
||||
failure_organization_permissions, p
|
||||
"Test 2 failed. Expected {failure_organization_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
@ -1021,8 +1011,7 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
|
||||
.await;
|
||||
if p != success_permissions {
|
||||
return Err(format!(
|
||||
"Test 3 failed. Expected {:?}, got {:?}",
|
||||
success_permissions, p
|
||||
"Test 3 failed. Expected {success_permissions:?}, got {p:?}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -487,7 +487,7 @@ async fn test_multi_get_redis_cache() {
|
||||
// Create 5 modpacks
|
||||
let mut modpacks = Vec::new();
|
||||
for i in 0..5 {
|
||||
let slug = format!("test-modpack-{}", i);
|
||||
let slug = format!("test-modpack-{i}");
|
||||
|
||||
let creation_data = get_public_project_creation_data(
|
||||
&slug,
|
||||
@ -503,7 +503,7 @@ async fn test_multi_get_redis_cache() {
|
||||
// Create 5 mods
|
||||
let mut mods = Vec::new();
|
||||
for i in 0..5 {
|
||||
let slug = format!("test-mod-{}", i);
|
||||
let slug = format!("test-mod-{i}");
|
||||
|
||||
let creation_data = get_public_project_creation_data(
|
||||
&slug,
|
||||
|
||||
@ -28,7 +28,7 @@ async fn oauth_flow_happy_path() {
|
||||
} = &env.dummy.oauth_client_alpha;
|
||||
|
||||
// Initiate authorization
|
||||
let redirect_uri = format!("{}?foo=bar", base_redirect_uri);
|
||||
let redirect_uri = format!("{base_redirect_uri}?foo=bar");
|
||||
let original_state = "1234";
|
||||
let resp = env
|
||||
.api
|
||||
|
||||
@ -81,7 +81,7 @@ pub async fn pat_full_test() {
|
||||
|
||||
// Change scopes and test again
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": 0,
|
||||
@ -93,7 +93,7 @@ pub async fn pat_full_test() {
|
||||
|
||||
// Change scopes back, and set expiry to the past, and test again
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": Scopes::COLLECTION_CREATE,
|
||||
@ -109,21 +109,21 @@ pub async fn pat_full_test() {
|
||||
|
||||
// Change everything back to normal and test again
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() + Duration::days(1), // no longer expired!
|
||||
}))
|
||||
.to_request();
|
||||
|
||||
println!("PAT ID FOR TEST: {}", id);
|
||||
println!("PAT ID FOR TEST: {id}");
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
assert_eq!(mock_pat_test(access_token).await, 200); // Works again
|
||||
|
||||
// Patching to a bad expiry should fail
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() - Duration::days(1), // Past
|
||||
@ -140,7 +140,7 @@ pub async fn pat_full_test() {
|
||||
}
|
||||
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": scope.bits(),
|
||||
@ -156,7 +156,7 @@ pub async fn pat_full_test() {
|
||||
// Delete PAT
|
||||
let req = test::TestRequest::delete()
|
||||
.append_pat(USER_USER_PAT)
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
@ -260,7 +260,7 @@ pub async fn bad_pats() {
|
||||
|
||||
// Patching to a bad expiry should fail
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() - Duration::days(1), // Past
|
||||
@ -277,7 +277,7 @@ pub async fn bad_pats() {
|
||||
}
|
||||
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.uri(&format!("/_internal/pat/{id}"))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": scope.bits(),
|
||||
|
||||
@ -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() });
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
})
|
||||
|
||||
@ -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"
|
||||
}
|
||||
12
packages/app-lib/.sqlx/query-27283e20fc86c941c7d6d09259d8f4ec2e248f751f98140f77bea4f9d5971ef1.json
generated
Normal file
12
packages/app-lib/.sqlx/query-27283e20fc86c941c7d6d09259d8f4ec2e248f751f98140f77bea4f9d5971ef1.json
generated
Normal 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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
20
packages/app-lib/.sqlx/query-a2184fc5d62570aec0a15c0a8d628a597e90c2bf7ce5dc1b39edb6977e2f6da6.json
generated
Normal file
20
packages/app-lib/.sqlx/query-a2184fc5d62570aec0a15c0a8d628a597e90c2bf7ce5dc1b39edb6977e2f6da6.json
generated
Normal 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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
12
packages/app-lib/.sqlx/query-df600f2615979ab61bfe235a04add18a4900021ee6ccfc165c9a6dad41046cba.json
generated
Normal file
12
packages/app-lib/.sqlx/query-df600f2615979ab61bfe235a04add18a4900021ee6ccfc165c9a6dad41046cba.json
generated
Normal 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"
|
||||
}
|
||||
32
packages/app-lib/.sqlx/query-fd834e256e142820f25305ccffaf07f736c5772045b973dcc10573b399111344.json
generated
Normal file
32
packages/app-lib/.sqlx/query-fd834e256e142820f25305ccffaf07f736c5772045b973dcc10573b399111344.json
generated
Normal 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"
|
||||
}
|
||||
@ -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 }
|
||||
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE profiles ADD COLUMN launcher_feature_version TEXT NOT NULL DEFAULT 'none'
|
||||
@ -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);
|
||||
@ -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}")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user