1
0

Migrate to SQLite for Internal Launcher Data (#1300)

* initial migration

* barebones profiles

* Finish profiles

* Add back file watcher

* UI support progress

* Finish most of cache

* Fix options page

* Fix forge, finish modrinth auth

* Accounts, process cache

* Run SQLX prepare

* Finish

* Run lint + actions

* Fix version to be compat with windows

* fix lint

* actually fix lint

* actually fix lint again
This commit is contained in:
Geometrically
2024-07-24 11:03:19 -07:00
committed by GitHub
parent 90f74427d9
commit 49a20a303a
156 changed files with 9208 additions and 8547 deletions

3
.gitignore vendored
View File

@@ -54,3 +54,6 @@ apps/frontend/src/generated
.turbo
target
generated
# app testing dir
app-playground-data/*

736
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
resolver = '2'
members = [
'./packages/app-lib',
'./packages/app-macros',
'./apps/app-playground',
'./apps/app'
]
@@ -14,3 +13,6 @@ codegen-units = 1 # Compile crates one after another so the compiler can optimiz
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros]
opt-level = 3

View File

@@ -1,7 +1,7 @@
{
"name": "@modrinth/app-frontend",
"private": true,
"version": "0.7.2",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, ref, onMounted } from 'vue'
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
import {
HomeIcon,
@@ -21,11 +21,11 @@ import SplashScreen from '@/components/ui/SplashScreen.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
import { handleError, useNotifications } from '@/store/notifications.js'
import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { MinimizeIcon, MaximizeIcon, ChatIcon } from '@/assets/icons'
import { type } from '@tauri-apps/api/os'
import { appWindow } from '@tauri-apps/api/window'
import { isDev, getOS, isOffline, showLauncherLogsFolder } from '@/helpers/utils.js'
import { isDev, getOS, showLauncherLogsFolder } from '@/helpers/utils.js'
import {
mixpanel_track,
mixpanel_init,
@@ -36,18 +36,27 @@ import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import { getVersion } from '@tauri-apps/api/app'
import { window as TauriWindow } from '@tauri-apps/api'
import { TauriEvent } from '@tauri-apps/api/event'
import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
import { confirm } from '@tauri-apps/api/dialog'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
import { install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
import { useInstall } from '@/store/install.js'
const themeStore = useTheming()
const urlModal = ref(null)
const isLoading = ref(true)
const offline = ref(false)
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const showOnboarding = ref(false)
const nativeDecorations = ref(false)
@@ -62,16 +71,16 @@ defineExpose({
const {
native_decorations,
theme,
opt_out_analytics,
telemetry,
collapsed_navigation,
advanced_rendering,
fully_onboarded,
onboarded,
} = await get()
// video should play if the user is not on linux, and has not onboarded
os.value = await getOS()
const dev = await isDev()
const version = await getVersion()
showOnboarding.value = !fully_onboarded
showOnboarding.value = !onboarded
nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') appWindow.setDecorations(native_decorations)
@@ -81,10 +90,10 @@ defineExpose({
themeStore.advancedRendering = advanced_rendering
mixpanel_init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' })
if (opt_out_analytics) {
if (telemetry) {
mixpanel_opt_out_tracking()
}
mixpanel_track('Launched', { version, dev, fully_onboarded })
mixpanel_track('Launched', { version, dev, onboarded })
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
@@ -94,11 +103,6 @@ defineExpose({
document.getElementsByTagName('html')[0].classList.add('windows')
}
offline.value = await isOffline()
await offline_listener((b) => {
offline.value = b
})
await warning_listener((e) =>
notificationsWrapper.value.addNotification({
title: 'Warning',
@@ -118,49 +122,10 @@ defineExpose({
},
})
const confirmClose = async () => {
const confirmed = await confirm(
'An action is currently in progress. Are you sure you want to exit?',
{
title: 'Modrinth',
type: 'warning',
},
)
return confirmed
}
const handleClose = async () => {
if (failureText.value != null) {
await TauriWindow.getCurrent().close()
return
}
// State should respond immeiately if it's safe to close
// If not, code is deadlocked or worse, so wait 2 seconds and then ask the user to confirm closing
// (Exception: if the user is changing config directory, which takes control of the state, and it's taking a significant amount of time for some reason)
const isSafe = await Promise.race([
check_safe_loading_bars_complete(),
new Promise((r) => setTimeout(r, 2000)),
])
if (!isSafe) {
const response = await confirmClose()
if (!response) {
return
}
}
await await_sync()
await TauriWindow.getCurrent().close()
}
const openSupport = async () => {
window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: 'https://discord.gg/modrinth',
},
})
}
TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose()
})
@@ -179,15 +144,22 @@ const loading = useLoading()
const notifications = useNotifications()
const notificationsWrapper = ref()
watch(notificationsWrapper, () => {
notifications.setNotifs(notificationsWrapper.value)
})
const error = useError()
const errorModal = ref()
watch(errorModal, () => {
const install = useInstall()
const modInstallModal = ref()
const installConfirmModal = ref()
const incompatibilityWarningModal = ref()
onMounted(() => {
notifications.setNotifs(notificationsWrapper.value)
error.setErrorModal(errorModal.value)
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
install.setInstallConfirmModal(installConfirmModal)
install.setModInstallModal(modInstallModal)
})
document.querySelector('body').addEventListener('click', function (e) {
@@ -284,7 +256,7 @@ command_listener(async (e) => {
<div class="button-row push-right">
<Button @click="showLauncherLogsFolder"><FileIcon />Open launcher logs</Button>
<Button @click="openSupport"><ChatIcon />Get support</Button>
<a class="btn" href="https://support.modrinth.com"> <ChatIcon /> Get support </a>
</div>
</Card>
</div>
@@ -385,6 +357,9 @@ command_listener(async (e) => {
<URLConfirmModal ref="urlModal" />
<Notifications ref="notificationsWrapper" />
<ErrorModal ref="errorModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<InstallConfirmModal ref="installConfirmModal" />
</template>
<style lang="scss" scoped>
@@ -584,55 +559,6 @@ command_listener(async (e) => {
}
}
.instance-list {
display: flex;
flex-direction: column;
justify-content: center;
width: 70%;
margin: 0.4rem;
p:nth-child(1) {
font-size: 0.6rem;
}
& > p {
color: var(--color-base);
margin: 0.8rem 0;
font-size: 0.7rem;
line-height: 0.8125rem;
font-weight: 500;
text-transform: uppercase;
}
}
.user-section {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 4.375rem;
section {
display: flex;
flex-direction: column;
justify-content: flex-start;
text-align: left;
margin-left: 0.5rem;
}
.username {
margin-bottom: 0.3rem;
font-weight: 400;
line-height: 1.25rem;
color: var(--color-contrast);
}
a {
font-weight: 400;
color: var(--color-secondary);
}
}
.nav-section {
display: flex;
flex-direction: column;
@@ -643,14 +569,6 @@ command_listener(async (e) => {
gap: 1rem;
}
.video {
margin-top: 2.25rem;
width: 100vw;
height: calc(100vh - 2.25rem);
object-fit: cover;
border-radius: var(--radius-md);
}
.button-row {
display: flex;
flex-direction: row;

View File

@@ -127,46 +127,46 @@ const sortBy = ref('Name')
const filteredResults = computed(() => {
let instances = props.instances.filter((instance) => {
return instance.metadata.name.toLowerCase().includes(search.value.toLowerCase())
return instance.name.toLowerCase().includes(search.value.toLowerCase())
})
if (sortBy.value === 'Name') {
instances.sort((a, b) => {
return a.metadata.name.localeCompare(b.metadata.name)
return a.name.localeCompare(b.name)
})
}
if (sortBy.value === 'Game version') {
instances.sort((a, b) => {
return a.metadata.game_version.localeCompare(b.metadata.game_version)
return a.game_version.localeCompare(b.game_version)
})
}
if (sortBy.value === 'Last played') {
instances.sort((a, b) => {
return dayjs(b.metadata.last_played ?? 0).diff(dayjs(a.metadata.last_played ?? 0))
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
})
}
if (sortBy.value === 'Date created') {
instances.sort((a, b) => {
return dayjs(b.metadata.date_created).diff(dayjs(a.metadata.date_created))
return dayjs(b.date_created).diff(dayjs(a.date_created))
})
}
if (sortBy.value === 'Date modified') {
instances.sort((a, b) => {
return dayjs(b.metadata.date_modified).diff(dayjs(a.metadata.date_modified))
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
})
}
if (filters.value === 'Custom instances') {
instances = instances.filter((instance) => {
return !instance.metadata?.linked_data
return !instance.linked_data
})
} else if (filters.value === 'Downloaded modpacks') {
instances = instances.filter((instance) => {
return instance.metadata?.linked_data
return instance.linked_data
})
}
@@ -174,7 +174,7 @@ const filteredResults = computed(() => {
if (group.value === 'Loader') {
instances.forEach((instance) => {
const loader = formatCategoryHeader(instance.metadata.loader)
const loader = formatCategoryHeader(instance.loader)
if (!instanceMap.has(loader)) {
instanceMap.set(loader, [])
}
@@ -183,19 +183,19 @@ const filteredResults = computed(() => {
})
} else if (group.value === 'Game version') {
instances.forEach((instance) => {
if (!instanceMap.has(instance.metadata.game_version)) {
instanceMap.set(instance.metadata.game_version, [])
if (!instanceMap.has(instance.game_version)) {
instanceMap.set(instance.game_version, [])
}
instanceMap.get(instance.metadata.game_version).push(instance)
instanceMap.get(instance.game_version).push(instance)
})
} else if (group.value === 'Category') {
instances.forEach((instance) => {
if (instance.metadata.groups.length === 0) {
instance.metadata.groups.push('None')
if (instance.groups.length === 0) {
instance.groups.push('None')
}
for (const category of instance.metadata.groups) {
for (const category of instance.groups) {
if (!instanceMap.has(category)) {
instanceMap.set(category, [])
}

View File

@@ -17,22 +17,15 @@ import Instance from '@/components/ui/Instance.vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import ProjectCard from '@/components/ui/ProjectCard.vue'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
kill_by_uuid,
} from '@/helpers/process.js'
import { get_by_profile_path } from '@/helpers/process.js'
import { handleError } from '@/store/notifications.js'
import { duplicate, remove, run } from '@/helpers/profile.js'
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
import { useRouter } from 'vue-router'
import { showProfileInFolder } from '@/helpers/utils.js'
import { useFetch } from '@/helpers/fetch.js'
import { install as pack_install } from '@/helpers/pack.js'
import { useTheming } from '@/store/state.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js'
const router = useRouter()
@@ -58,9 +51,7 @@ const modsRow = ref(null)
const instanceOptions = ref(null)
const instanceComponents = ref(null)
const rows = ref(null)
const confirmModal = ref(null)
const deleteConfirmModal = ref(null)
const modInstallModal = ref(null)
const themeStore = useTheming()
const currentDeleteInstance = ref(null)
@@ -90,23 +81,24 @@ const handleInstanceRightClick = async (event, passedInstance) => {
},
]
const running = await get_all_running_profile_paths().catch(handleError)
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
const options = running.includes(passedInstance.path)
? [
{
name: 'stop',
color: 'danger',
},
...baseOptions,
]
: [
{
name: 'play',
color: 'primary',
},
...baseOptions,
]
const options =
runningProcesses.length > 0
? [
{
name: 'stop',
color: 'danger',
},
...baseOptions,
]
: [
{
name: 'play',
color: 'primary',
},
...baseOptions,
]
instanceOptions.value.showMenu(event, passedInstance, options)
}
@@ -132,22 +124,20 @@ const handleOptionsClick = async (args) => {
case 'play':
await run(args.item.path).catch(handleSevereError)
mixpanel_track('InstanceStart', {
loader: args.item.metadata.loader,
game_version: args.item.metadata.game_version,
loader: args.item.loader,
game_version: args.item.game_version,
})
break
case 'stop':
for (const u of await get_uuids_by_profile_path(args.item.path).catch(handleError)) {
await kill_by_uuid(u).catch(handleError)
}
await kill(args.item.path).catch(handleError)
mixpanel_track('InstanceStop', {
loader: args.item.metadata.loader,
game_version: args.item.metadata.game_version,
loader: args.item.loader,
game_version: args.item.game_version,
})
break
case 'add_content':
await router.push({
path: `/browse/${args.item.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: args.item.path },
})
break
@@ -170,21 +160,8 @@ const handleOptionsClick = async (args) => {
await navigator.clipboard.writeText(args.item.path)
break
case 'install': {
const versions = await useFetch(
`https://api.modrinth.com/v2/project/${args.item.project_id}/version`,
'project versions',
)
await installVersion(args.item.project_id, null, null, 'ProjectCardContextMenu')
if (args.item.project_type === 'modpack') {
await pack_install(
args.item.project_id,
versions[0].id,
args.item.title,
args.item.icon_url,
)
} else {
modInstallModal.value.show(args.item.project_id, versions)
}
break
}
case 'open_link':
@@ -243,7 +220,7 @@ onUnmounted(() => {
<router-link :to="row.route">{{ row.label }}</router-link>
<ChevronRightIcon />
</div>
<section v-if="row.instances[0].metadata" ref="modsRow" class="instances">
<section v-if="row.instance" ref="modsRow" class="instances">
<Instance
v-for="instance in row.instances.slice(0, maxInstancesPerRow)"
:key="(instance?.project_id || instance?.id) + instance.install_stage"
@@ -258,8 +235,6 @@ onUnmounted(() => {
ref="instanceComponents"
class="item"
:project="project"
:confirm-modal="confirmModal"
:mod-install-modal="modInstallModal"
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
/>
</section>
@@ -278,8 +253,6 @@ onUnmounted(() => {
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
</ContextMenu>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
</template>
<style lang="scss" scoped>
.content {

View File

@@ -2,7 +2,7 @@
import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets'
import { Button, OverflowMenu } from '@modrinth/ui'
import { open } from '@tauri-apps/api/dialog'
import { add_project_from_path, get } from '@/helpers/profile.js'
import { add_project_from_path } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router'
@@ -20,14 +20,13 @@ const handleAddContentFromFile = async () => {
if (!newProject) return
for (const project of newProject) {
await add_project_from_path(props.instance.path, project, 'mod').catch(handleError)
await add_project_from_path(props.instance.path, project).catch(handleError)
}
props.instance.initProjects(await get(props.instance.path).catch(handleError))
}
const handleSearchContent = async () => {
await router.push({
path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.path },
})
}

View File

@@ -60,9 +60,9 @@ defineExpose({
})
const isLinkedData = (item) => {
if (item.instance != undefined && item.instance.metadata.linked_data) {
if (item.instance != undefined && item.instance.linked_data) {
return true
} else if (item.metadata != undefined && item.metadata.linked_data) {
} else if (item != undefined && item.linked_data) {
return true
}
return false

View File

@@ -23,7 +23,7 @@ defineExpose({
})
const exportModal = ref(null)
const nameInput = ref(props.instance.metadata.name)
const nameInput = ref(props.instance.name)
const exportDescription = ref('')
const versionInput = ref('1.0.0')
const files = ref([])

View File

@@ -1,22 +1,14 @@
<script setup>
import { onUnmounted, ref, watch } from 'vue'
import { onUnmounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { DownloadIcon, StopCircleIcon, PlayIcon } from '@modrinth/assets'
import { StopCircleIcon, PlayIcon } from '@modrinth/assets'
import { Card, Avatar, AnimatedLogo } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import { install as pack_install } from '@/helpers/pack'
import { list, run } from '@/helpers/profile'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
kill_by_uuid,
} from '@/helpers/process'
import { kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
import { process_listener } from '@/helpers/events'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/state.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { handleSevereError } from '@/store/error.js'
@@ -29,107 +21,31 @@ const props = defineProps({
},
})
const confirmModal = ref(null)
const modInstallModal = ref(null)
const playing = ref(false)
const uuid = ref(null)
const modLoading = ref(
props.instance.install_stage ? props.instance.install_stage !== 'installed' : false,
)
watch(
() => props.instance,
() => {
modLoading.value = props.instance.install_stage
? props.instance.install_stage !== 'installed'
: false
},
)
const modLoading = computed(() => props.instance.install_stage !== 'installed')
const router = useRouter()
const seeInstance = async () => {
const instancePath = props.instance.metadata
? `/instance/${encodeURIComponent(props.instance.path)}/`
: `/project/${encodeURIComponent(props.instance.project_id)}/`
await router.push(instancePath)
await router.push(`/instance/${encodeURIComponent(props.instance.path)}/`)
}
const checkProcess = async () => {
const runningPaths = await get_all_running_profile_paths().catch(handleError)
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
if (runningPaths.includes(props.instance.path)) {
playing.value = true
return
}
playing.value = false
uuid.value = null
}
const install = async (e) => {
e?.stopPropagation()
modLoading.value = true
const versions = await useFetch(
`https://api.modrinth.com/v2/project/${props.instance.project_id}/version`,
'project versions',
)
if (props.instance.project_type === 'modpack') {
const packs = Object.values(await list(true).catch(handleError))
if (
packs.length === 0 ||
!packs
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === props.instance.project_id)
) {
modLoading.value = true
await pack_install(
props.instance.project_id,
versions[0].id,
props.instance.title,
props.instance.icon_url,
).catch(handleError)
modLoading.value = false
mixpanel_track('PackInstall', {
id: props.instance.project_id,
version_id: versions[0].id,
title: props.instance.title,
source: 'InstanceCard',
})
} else
confirmModal.value.show(
props.instance.project_id,
versions[0].id,
props.instance.title,
props.instance.icon_url,
)
} else {
modInstallModal.value.show(
props.instance.project_id,
versions,
props.instance.title,
props.instance.project_type,
)
}
modLoading.value = false
playing.value = runningProcesses.length > 0
}
const play = async (e, context) => {
e?.stopPropagation()
modLoading.value = true
uuid.value = await run(props.instance.path).catch(handleSevereError)
await run(props.instance.path).catch(handleSevereError)
modLoading.value = false
playing.value = true
mixpanel_track('InstancePlay', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
}
@@ -138,22 +54,13 @@ const stop = async (e, context) => {
e?.stopPropagation()
playing.value = false
// If we lost the uuid for some reason, such as a user navigating
// from-then-back to this page, we will get all uuids by the instance path.
// For-each uuid, kill the process.
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(props.instance.path).catch(handleError)
uuid.value = uuids[0]
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError) // If we still have the uuid, just kill it
await kill(props.instance.path).catch(handleError)
mixpanel_track('InstanceStop', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
uuid.value = null
}
const openFolder = async () => {
@@ -162,14 +69,12 @@ const openFolder = async () => {
const addContent = async () => {
await router.push({
path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.path },
})
}
defineExpose({
install,
playing,
play,
stop,
seeInstance,
@@ -179,7 +84,7 @@ defineExpose({
})
const unlisten = await process_listener((e) => {
if (e.event === 'finished' && e.uuid === uuid.value) playing.value = false
if (e.event === 'finished' && e.profile_path_id === props.instance.path) playing.value = false
})
onUnmounted(() => unlisten())
@@ -190,46 +95,32 @@ onUnmounted(() => unlisten())
<Card class="instance-card-item button-base" @click="seeInstance" @mouseenter="checkProcess">
<Avatar
size="lg"
:src="
props.instance.metadata
? !props.instance.metadata.icon ||
(props.instance.metadata.icon && props.instance.metadata.icon.startsWith('http'))
? props.instance.metadata.icon
: convertFileSrc(props.instance.metadata?.icon)
: props.instance.icon_url
"
:src="props.instance.icon_path ? convertFileSrc(props.instance.icon_path) : null"
alt="Mod card"
class="mod-image"
/>
<div class="project-info">
<p class="title">{{ props.instance.metadata?.name || props.instance.title }}</p>
<p class="title">{{ props.instance.name }}</p>
<p class="description">
{{ props.instance.metadata?.loader }}
{{ props.instance.metadata?.game_version || props.instance.latest_version }}
{{ props.instance.loader }}
{{ props.instance.game_version }}
</p>
</div>
</Card>
<div
v-if="props.instance.metadata && playing === false && modLoading === false"
class="install cta button-base"
@click="(e) => play(e, 'InstanceCard')"
>
<PlayIcon />
</div>
<div v-else-if="modLoading === true && playing === false" class="cta loading-cta">
<AnimatedLogo class="loading-indicator" />
</div>
<div
v-else-if="playing === true"
v-if="playing === true"
class="stop cta button-base"
@click="(e) => stop(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<StopCircleIcon />
</div>
<div v-else class="install cta button-base" @click="install"><DownloadIcon /></div>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
<div v-else-if="modLoading === true && playing === false" class="cta loading-cta">
<AnimatedLogo class="loading-indicator" />
</div>
<div v-else class="install cta button-base" @click="(e) => play(e, 'InstanceCard')">
<PlayIcon />
</div>
</div>
</template>

View File

@@ -213,13 +213,7 @@ import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile'
import { open } from '@tauri-apps/api/dialog'
import { tauri } from '@tauri-apps/api'
import {
get_game_versions,
get_fabric_versions,
get_forge_versions,
get_quilt_versions,
get_neoforge_versions,
} from '@/helpers/metadata'
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect'
import { mixpanel_track } from '@/helpers/mixpanel'
@@ -304,10 +298,10 @@ const [
all_game_versions,
loaders,
] = await Promise.all([
get_fabric_versions().then(shallowRef).catch(handleError),
get_forge_versions().then(shallowRef).catch(handleError),
get_quilt_versions().then(shallowRef).catch(handleError),
get_neoforge_versions().then(shallowRef).catch(handleError),
get_loader_versions('fabric').then(shallowRef).catch(handleError),
get_loader_versions('forge').then(shallowRef).catch(handleError),
get_loader_versions('quilt').then(shallowRef).catch(handleError),
get_loader_versions('neo').then(shallowRef).catch(handleError),
get_game_versions().then(shallowRef).catch(handleError),
get_loaders()
.then((value) =>

View File

@@ -53,9 +53,6 @@ defineExpose({
show: async (version, currentSelectedJava) => {
chosenInstallOptions.value = await find_filtered_jres(version).catch(handleError)
console.log(chosenInstallOptions.value)
console.log(version)
currentSelected.value = currentSelectedJava
if (!currentSelected.value) {
currentSelected.value = { path: '', version: '' }

View File

@@ -162,7 +162,6 @@ async function reinstallJava() {
const path = await auto_install_java(props.version).catch(handleError)
let result = await get_jre(path)
console.log('java result ' + result)
if (!result) {
result = {
path: path,
@@ -205,6 +204,10 @@ async function reinstallJava() {
align-items: center;
gap: 0.5rem;
margin: 0;
.btn {
width: max-content;
}
}
.test-success {

View File

@@ -29,7 +29,7 @@ const filteredVersions = computed(() => {
})
const modpackVersionModal = ref(null)
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.version_id)
const installedVersion = computed(() => props.instance?.linked_data?.version_id)
const installing = computed(() => props.instance.install_stage !== 'installed')
const inProgress = ref(false)
@@ -50,7 +50,7 @@ const switchVersion = async (versionId) => {
:noblur="!themeStore.advancedRendering"
>
<div class="modal-body">
<Card v-if="instance.metadata.linked_data" class="mod-card">
<Card v-if="instance.linked_data" class="mod-card">
<div class="table">
<div class="table-row with-columns table-head">
<div class="table-cell table-text download-cell" />

View File

@@ -6,10 +6,8 @@ import { computed, ref } from 'vue'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRouter } from 'vue-router'
import { useFetch } from '@/helpers/fetch.js'
import { list } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { install as pack_install } from '@/helpers/pack.js'
import { install as installVersion } from '@/store/install.js'
dayjs.extend(relativeTime)
const router = useRouter()
@@ -22,18 +20,6 @@ const props = defineProps({
return {}
},
},
confirmModal: {
type: Object,
default() {
return {}
},
},
modInstallModal: {
type: Object,
default() {
return {}
},
},
})
const toColor = computed(() => {
@@ -65,40 +51,15 @@ const toTransparent = computed(() => {
const install = async (e) => {
e?.stopPropagation()
installing.value = true
const versions = await useFetch(
`https://api.modrinth.com/v2/project/${props.project.project_id}/version`,
'project versions',
)
if (props.project.project_type === 'modpack') {
const packs = Object.values(await list(true).catch(handleError))
if (
packs.length === 0 ||
!packs
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === props.project.project_id)
) {
installing.value = true
await pack_install(
props.project.project_id,
versions[0].id,
props.project.title,
props.project.icon_url,
).catch(handleError)
await installVersion(
props.project.project_id,
null,
props.instance ? props.instance.path : null,
'ProjectCard',
() => {
installing.value = false
} else
props.confirmModal.show(
props.project.project_id,
versions[0].id,
props.project.title,
props.project.icon_url,
)
} else {
props.modInstallModal.show(props.project.project_id, versions)
}
installing.value = false
},
)
}
</script>

View File

@@ -15,15 +15,15 @@
</Button>
<div v-if="offline" class="status">
<span class="circle stopped" />
<div class="running-text clickable" @click="refreshInternet()">
<div class="running-text">
<span> Offline </span>
</div>
</div>
<div v-if="selectedProfile" class="status">
<div v-if="selectedProcess" class="status">
<span class="circle running" />
<div ref="profileButton" class="running-text">
<router-link :to="`/instance/${encodeURIComponent(selectedProfile.path)}`">
{{ selectedProfile.metadata.name }}
<router-link :to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`">
{{ selectedProcess.profile.name }}
</router-link>
<div
v-if="currentProcesses.length > 1"
@@ -34,7 +34,12 @@
<DropdownIcon />
</div>
</div>
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop()">
<Button
v-tooltip="'Stop instance'"
icon-only
class="icon-button stop"
@click="stop(selectedProcess)"
>
<StopCircleIcon />
</Button>
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
@@ -75,17 +80,17 @@
class="profile-card"
>
<Button
v-for="profile in currentProcesses"
:key="profile.id"
v-for="process in currentProcesses"
:key="process.pid"
class="profile-button"
@click="selectProfile(profile)"
@click="selectedProcess(process)"
>
<div class="text"><span class="circle running" /> {{ profile.metadata.name }}</div>
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
<Button
v-tooltip="'Stop instance'"
icon-only
class="icon-button stop"
@click.stop="stop(profile.path)"
@click.stop="stop(process)"
>
<StopCircleIcon />
</Button>
@@ -93,7 +98,7 @@
v-tooltip="'View logs'"
icon-only
class="icon-button"
@click.stop="goToTerminal(profile.path)"
@click.stop="goToTerminal(process.profile.path)"
>
<TerminalSquareIcon />
</Button>
@@ -106,19 +111,15 @@
import { DownloadIcon, StopCircleIcon, TerminalSquareIcon, DropdownIcon } from '@modrinth/assets'
import { Button, Card } from '@modrinth/ui'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import {
get_all_running_profiles as getRunningProfiles,
kill_by_uuid as killProfile,
get_uuids_by_profile_path as getProfileProcesses,
} from '@/helpers/process'
import { loading_listener, process_listener, offline_listener } from '@/helpers/events'
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
import { loading_listener, process_listener } from '@/helpers/events'
import { useRouter } from 'vue-router'
import { progress_bars_list } from '@/helpers/state.js'
import { refreshOffline, isOffline } from '@/helpers/utils.js'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { ChatIcon } from '@/assets/icons'
import { get_many } from '@/helpers/profile.js'
const router = useRouter()
const card = ref(null)
@@ -129,38 +130,44 @@ const showCard = ref(false)
const showProfiles = ref(false)
const currentProcesses = ref(await getRunningProfiles().catch(handleError))
const selectedProfile = ref(currentProcesses.value[0])
const currentProcesses = ref([])
const selectedProcess = ref()
const offline = ref(await isOffline().catch(handleError))
const refreshInternet = async () => {
offline.value = await refreshOffline().catch(handleError)
const refresh = async () => {
const processes = await getRunningProcesses().catch(handleError)
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
currentProcesses.value = processes.map((x) => ({
profile: profiles.find((prof) => x.profile_path === prof.path),
...x,
}))
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
selectedProcess.value = currentProcesses.value[0]
}
}
await refresh()
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const unlistenProcess = await process_listener(async () => {
await refresh()
})
const unlistenRefresh = await offline_listener(async (b) => {
offline.value = b
await refresh()
})
const refresh = async () => {
currentProcesses.value = await getRunningProfiles().catch(handleError)
if (!currentProcesses.value.includes(selectedProfile.value)) {
selectedProfile.value = currentProcesses.value[0]
}
}
const stop = async (path) => {
const stop = async (process) => {
try {
const processes = await getProfileProcesses(path ?? selectedProfile.value.path)
await killProfile(processes[0])
console.log(process.pid)
await killProcess(process.pid).catch(handleError)
mixpanel_track('InstanceStop', {
loader: currentProcesses.value[0].metadata.loader,
game_version: currentProcesses.value[0].metadata.game_version,
loader: process.profile.loader,
game_version: process.profile.game_version,
source: 'AppBar',
})
} catch (e) {
@@ -170,7 +177,7 @@ const stop = async (path) => {
}
const goToTerminal = (path) => {
router.push(`/instance/${encodeURIComponent(path ?? selectedProfile.value.path)}/logs`)
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
}
const currentLoadingBars = ref([])
@@ -182,8 +189,8 @@ const refreshInfo = async () => {
if (x.bar_type.type === 'java_download') {
x.title = 'Downloading Java ' + x.bar_type.version
}
if (x.bar_type.profile_name) {
x.title = x.bar_type.profile_name
if (x.bar_type.profile_path) {
x.title = x.bar_type.profile_path
}
if (x.bar_type.pack_name) {
x.title = x.bar_type.pack_name
@@ -215,8 +222,8 @@ const unlistenLoading = await loading_listener(async () => {
await refreshInfo()
})
const selectProfile = (profile) => {
selectedProfile.value = profile
const selectProcess = (process) => {
selectedProcess.value = process
showProfiles.value = false
}
@@ -267,7 +274,6 @@ onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutsideProfile)
unlistenProcess()
unlistenLoading()
unlistenRefresh()
})
</script>

View File

@@ -69,12 +69,7 @@ import { formatNumber, formatCategory } from '@modrinth/utils'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { ref } from 'vue'
import { add_project_from_version as installMod, list } from '@/helpers/profile.js'
import { install as packInstall } from '@/helpers/pack.js'
import { installVersionDependencies } from '@/helpers/utils.js'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { install as installVersion } from '@/store/install.js'
dayjs.extend(relativeTime)
const props = defineProps({
@@ -94,18 +89,6 @@ const props = defineProps({
type: Object,
default: null,
},
confirmModal: {
type: Object,
default: null,
},
modInstallModal: {
type: Object,
default: null,
},
incompatibilityWarningModal: {
type: Object,
default: null,
},
featured: {
type: Boolean,
default: false,
@@ -123,93 +106,19 @@ const installed = ref(props.installed)
async function install() {
installing.value = true
const versions = await useFetch(
`https://api.modrinth.com/v2/project/${props.project.project_id}/version`,
'project versions',
)
let queuedVersionData
if (!props.instance) {
queuedVersionData = versions[0]
} else {
queuedVersionData = versions.find(
(v) =>
v.game_versions.includes(props.instance.metadata.game_version) &&
(props.project.project_type !== 'mod' ||
v.loaders.includes(props.instance.metadata.loader)),
)
}
if (props.project.project_type === 'modpack') {
const packs = Object.values(await list().catch(handleError))
if (
packs.length === 0 ||
!packs
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === props.project.project_id)
) {
await packInstall(
props.project.project_id,
queuedVersionData.id,
props.project.title,
props.project.icon_url,
).catch(handleError)
mixpanel_track('PackInstall', {
id: props.project.project_id,
version_id: queuedVersionData.id,
title: props.project.title,
source: 'SearchCard',
})
} else {
props.confirmModal.show(
props.project.project_id,
queuedVersionData.id,
props.project.title,
props.project.icon_url,
)
}
} else {
if (props.instance) {
if (!queuedVersionData) {
props.incompatibilityWarningModal.show(
props.instance,
props.project.title,
versions,
() => (installed.value = true),
props.project.project_id,
props.project.project_type,
)
installing.value = false
return
} else {
await installMod(props.instance.path, queuedVersionData.id).catch(handleError)
await installVersionDependencies(props.instance, queuedVersionData)
mixpanel_track('ProjectInstall', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
id: props.project.project_id,
project_type: props.project.project_type,
version_id: queuedVersionData.id,
title: props.project.title,
source: 'SearchCard',
})
}
} else {
props.modInstallModal.show(
props.project.project_id,
versions,
props.project.title,
props.project.project_type,
)
await installVersion(
props.project.project_id,
null,
props.instance ? props.instance.path : null,
'SearchCard',
(version) => {
installing.value = false
return
}
if (props.instance) installed.value = true
}
installing.value = false
if (props.instance && version) {
installed.value = true
}
},
)
}
</script>

View File

@@ -1,77 +1,37 @@
<script setup>
import { Modal, Button } from '@modrinth/ui'
import { ref } from 'vue'
import { useFetch } from '@/helpers/fetch.js'
import SearchCard from '@/components/ui/SearchCard.vue'
import { get_categories } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js'
import { install as packInstall } from '@/helpers/pack.js'
import mixpanel from 'mixpanel-browser'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import { get_version, get_project } from '@/helpers/cache.js'
import { install as installVersion } from '@/store/install.js'
const confirmModal = ref(null)
const project = ref(null)
const version = ref(null)
const categories = ref(null)
const installing = ref(false)
const modInstallModal = ref(null)
defineExpose({
async show(event) {
if (event.event === 'InstallVersion') {
version.value = await useFetch(
`https://api.modrinth.com/v2/version/${encodeURIComponent(event.id)}`,
'version',
)
project.value = await useFetch(
`https://api.modrinth.com/v2/project/${encodeURIComponent(version.value.project_id)}`,
'project',
)
version.value = await get_version(event.id).catch(handleError)
project.value = await get_project(version.value.project_id).catch(handleError)
} else {
project.value = await useFetch(
`https://api.modrinth.com/v2/project/${encodeURIComponent(event.id)}`,
'project',
)
version.value = await useFetch(
`https://api.modrinth.com/v2/version/${encodeURIComponent(project.value.versions[0])}`,
'version',
)
project.value = await get_project(event.id).catch(handleError)
version.value = await get_version(project.value.versions[0]).catch(handleError)
}
categories.value = (await get_categories().catch(handleError)).filter(
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
)
confirmModal.value.show()
categories.value = (await get_categories().catch(handleError)).filter(
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
)
confirmModal.value.show()
},
})
async function install() {
confirmModal.value.hide()
if (project.value.project_type === 'modpack') {
await packInstall(
project.value.id,
version.value.id,
project.value.title,
project.value.icon_url,
).catch(handleError)
mixpanel.track('PackInstall', {
id: project.value.id,
version_id: version.value.id,
title: project.value.title,
source: 'ProjectPage',
})
} else {
modInstallModal.value.show(
project.value.id,
[version.value],
project.value.title,
project.value.project_type,
)
}
await installVersion(project.value.id, version.value.id, null, 'URLConfirmModal')
}
</script>
@@ -96,7 +56,6 @@ async function install() {
</div>
</div>
</Modal>
<ModInstallModal ref="modInstallModal" />
</template>
<style scoped lang="scss">

View File

@@ -3,6 +3,7 @@
ref="incompatibleModal"
header="Incompatibility warning"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body">
<p>
@@ -12,13 +13,11 @@
</p>
<table>
<tr class="header">
<th>{{ instance?.metadata.name }}</th>
<th>{{ projectTitle }}</th>
<th>{{ instance?.name }}</th>
<th>{{ project.title }}</th>
</tr>
<tr class="content">
<td class="data">
{{ instance?.metadata.loader }} {{ instance?.metadata.game_version }}
</td>
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
<td>
<DropdownSelect
v-if="versions?.length > 1"
@@ -68,34 +67,25 @@ const themeStore = useTheming()
const instance = ref(null)
const project = ref(null)
const projectType = ref(null)
const projectTitle = ref(null)
const versions = ref(null)
const selectedVersion = ref(null)
const incompatibleModal = ref(null)
const installing = ref(false)
let markInstalled = () => {}
let onInstall = ref(() => {})
defineExpose({
show: (
instanceVal,
projectTitleVal,
selectedVersions,
extMarkInstalled,
projectIdVal,
projectTypeVal,
) => {
show: (instanceVal, projectVal, projectVersions, callback) => {
instance.value = instanceVal
projectTitle.value = projectTitleVal
versions.value = selectedVersions
selectedVersion.value = selectedVersions[0]
versions.value = projectVersions
selectedVersion.value = projectVersions[0]
project.value = projectIdVal
projectType.value = projectTypeVal
project.value = projectVal
onInstall.value = callback
installing.value = false
incompatibleModal.value.show()
markInstalled = extMarkInstalled
mixpanel_track('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
@@ -105,16 +95,16 @@ const install = async () => {
installing.value = true
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
installing.value = false
markInstalled()
onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide()
mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
loader: instance.value.loader,
game_version: instance.value.game_version,
id: project.value,
version_id: selectedVersion.value.id,
project_type: projectType.value,
title: projectTitle.value,
project_type: project.value.project_type,
title: project.value.title,
source: 'ProjectIncompatibilityWarningModal',
})
}

View File

@@ -9,47 +9,55 @@ import { handleError } from '@/store/state.js'
const themeStore = useTheming()
const version = ref('')
const title = ref('')
const projectId = ref('')
const icon = ref('')
const versionId = ref()
const project = ref()
const confirmModal = ref(null)
const installing = ref(false)
let onInstall = ref(() => {})
defineExpose({
show: (projectIdVal, versionId, projectTitle, projectIcon) => {
projectId.value = projectIdVal
version.value = versionId
title.value = projectTitle
icon.value = projectIcon
show: (projectVal, versionIdVal, callback) => {
project.value = projectVal
versionId.value = versionIdVal
installing.value = false
confirmModal.value.show()
onInstall.value = callback
mixpanel_track('PackInstallStart')
},
})
async function install() {
installing.value = true
console.log(`Installing ${projectId.value} ${version.value} ${title.value} ${icon.value}`)
confirmModal.value.hide()
await pack_install(
projectId.value,
version.value,
title.value,
icon.value ? icon.value : null,
project.value.id,
versionId.value,
project.value.title,
project.value.icon_url,
).catch(handleError)
mixpanel_track('PackInstall', {
id: projectId.value,
version_id: version.value,
title: title.value,
id: project.value.id,
version_id: versionId.value,
title: project.value.title,
source: 'ConfirmModal',
})
onInstall.value(versionId.value)
installing.value = false
}
</script>
<template>
<Modal ref="confirmModal" header="Are you sure?" :noblur="!themeStore.advancedRendering">
<Modal
ref="confirmModal"
header="Are you sure?"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body">
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right">

View File

@@ -14,10 +14,10 @@ import {
check_installed,
get,
list,
create,
} from '@/helpers/profile'
import { open } from '@tauri-apps/api/dialog'
import { create } from '@/helpers/profile'
import { installVersionDependencies } from '@/helpers/utils'
import { installVersionDependencies } from '@/store/install.js'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js'
@@ -27,13 +27,12 @@ import { tauri } from '@tauri-apps/api'
const themeStore = useTheming()
const router = useRouter()
const versions = ref([])
const project = ref('')
const projectTitle = ref('')
const projectType = ref('')
const versions = ref()
const project = ref()
const installModal = ref(null)
const installModal = ref()
const searchFilter = ref('')
const showCreation = ref(false)
const icon = ref(null)
const name = ref(null)
@@ -42,33 +41,65 @@ const loader = ref(null)
const gameVersion = ref(null)
const creatingInstance = ref(false)
defineExpose({
show: async (projectId, selectedVersions, title, type) => {
project.value = projectId
versions.value = selectedVersions
projectTitle.value = title
projectType.value = type
const profiles = ref([])
installModal.value.show()
const shownProfiles = computed(() =>
profiles.value
.filter((profile) => {
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
})
.filter((profile) => {
let loaders = versions.value.flatMap((v) => v.loaders)
return (
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
(project.value.project_type === 'mod'
? loaders.includes(profile.loader) || loaders.includes('minecraft')
: true)
)
}),
)
let onInstall = ref(() => {})
defineExpose({
show: async (projectVal, versionsVal, callback) => {
project.value = projectVal
versions.value = versionsVal
searchFilter.value = ''
profiles.value = await getData()
showCreation.value = false
name.value = null
icon.value = null
display_icon.value = null
gameVersion.value = null
loader.value = null
onInstall.value = callback
const profilesVal = await list().catch(handleError)
for (let profile of profilesVal) {
profile.installing = false
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
handleError,
)
}
profiles.value = profilesVal
installModal.value.show()
mixpanel_track('ProjectInstallStart', { source: 'ProjectInstallModal' })
},
})
const profiles = ref([])
async function install(instance) {
instance.installing = true
const version = versions.value.find((v) => {
return (
v.game_versions.includes(instance.metadata.game_version) &&
(v.loaders.includes(instance.metadata.loader) ||
v.loaders.includes('minecraft') ||
v.loaders.includes('iris') ||
v.loaders.includes('optifine'))
v.game_versions.includes(instance.game_version) &&
(project.value.project_type === 'mod'
? v.loaders.includes(instance.loader) || v.loaders.includes('minecraft')
: true)
)
})
@@ -85,45 +116,18 @@ async function install(instance) {
instance.installing = false
mixpanel_track('ProjectInstall', {
loader: instance.metadata.loader,
game_version: instance.metadata.game_version,
id: project.value,
loader: instance.loader,
game_version: instance.game_version,
id: project.value.id,
version_id: version.id,
project_type: projectType.value,
title: projectTitle.value,
project_type: project.value.project_type,
title: project.value.title,
source: 'ProjectInstallModal',
})
onInstall.value(version.id)
}
async function getData() {
const projects = await list(true).then(Object.values).catch(handleError)
const filtered = projects
.filter((profile) => {
return profile.metadata.name.toLowerCase().includes(searchFilter.value.toLowerCase())
})
.filter((profile) => {
return (
versions.value.flatMap((v) => v.game_versions).includes(profile.metadata.game_version) &&
versions.value
.flatMap((v) => v.loaders)
.some(
(value) =>
value === profile.metadata.loader ||
['minecraft', 'iris', 'optifine'].includes(value),
)
)
})
for (let profile of filtered) {
profile.installing = false
profile.installedMod = await check_installed(profile.path, project.value).catch(handleError)
}
return filtered
}
const alreadySentCreation = ref(false)
const toggleCreation = () => {
showCreation.value = !showCreation.value
name.value = null
@@ -132,8 +136,7 @@ const toggleCreation = () => {
gameVersion.value = null
loader.value = null
if (!alreadySentCreation.value) {
alreadySentCreation.value = false
if (showCreation.value) {
mixpanel_track('InstanceCreateStart', { source: 'ProjectInstallModal' })
}
}
@@ -197,18 +200,16 @@ const createInstance = async () => {
game_version: versions.value[0].game_versions[0],
id: project.value,
version_id: versions.value[0].id,
project_type: projectType.value,
title: projectTitle.value,
project_type: project.value.project_type,
title: project.value.title,
source: 'ProjectInstallModal',
})
onInstall.value(versions.value[0].id)
if (installModal.value) installModal.value.hide()
creatingInstance.value = false
}
const check_valid = computed(() => {
return name.value
})
</script>
<template>
@@ -216,6 +217,7 @@ const check_valid = computed(() => {
ref="installModal"
header="Install project to instance"
:noblur="!themeStore.advancedRendering"
:on-hide="onInstall"
>
<div class="modal-body">
<input
@@ -226,34 +228,27 @@ const check_valid = computed(() => {
placeholder="Search for an instance"
/>
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
<div v-for="profile in profiles" :key="profile.metadata.name" class="option">
<Button
transparent
class="profile-button"
@click="$router.push(`/instance/${encodeURIComponent(profile.path)}`)"
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
<router-link
class="btn btn-transparent profile-button"
:to="`/instance/${encodeURIComponent(profile.path)}`"
@click="installModal.hide()"
>
<Avatar
:src="
!profile.metadata.icon ||
(profile.metadata.icon && profile.metadata.icon.startsWith('http'))
? profile.metadata.icon
: tauri.convertFileSrc(profile.metadata?.icon)
"
:src="profile.icon_path ? tauri.convertFileSrc(profile.icon_path) : null"
class="profile-image"
/>
{{ profile.metadata.name }}
</Button>
{{ profile.name }}
</router-link>
<div
v-tooltip="
profile.metadata.linked_data?.locked && !profile.installedMod
profile.linked_data?.locked && !profile.installedMod
? 'Unpair or unlock an instance to add mods.'
: ''
"
>
<Button
:disabled="
profile.installedMod || profile.installing || profile.metadata.linked_data?.locked
"
:disabled="profile.installedMod || profile.installing || profile.linked_data?.locked"
@click="install(profile)"
>
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
@@ -263,7 +258,7 @@ const check_valid = computed(() => {
? 'Installing...'
: profile.installedMod
? 'Installed'
: profile.metadata.linked_data && profile.metadata.linked_data.locked
: profile.linked_data && profile.linked_data.locked
? 'Paired'
: 'Install'
}}
@@ -294,7 +289,7 @@ const check_valid = computed(() => {
placeholder="Name"
class="creation-input"
/>
<Button :disabled="creatingInstance === true || !check_valid" @click="createInstance()">
<Button :disabled="creatingInstance === true || !name" @click="createInstance()">
<RightArrowIcon />
{{ creatingInstance ? 'Creating...' : 'Create' }}
</Button>

View File

@@ -1,6 +1,6 @@
<script setup>
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
import { Button, Card, Checkbox } from '@modrinth/ui'
import { Button, Card, Checkbox, Modal } from '@modrinth/ui'
import {
DiscordIcon,
GithubIcon,
@@ -9,31 +9,57 @@ import {
SteamIcon,
GitLabIcon,
} from '@/assets/external'
import {
authenticate_begin_flow,
authenticate_await_completion,
login_2fa,
create_account,
login_pass,
} from '@/helpers/mr_auth.js'
import { login, login_2fa, create_account, login_pass } from '@/helpers/mr_auth.js'
import { handleError, useNotifications } from '@/store/state.js'
import { onMounted, ref } from 'vue'
import { ref } from 'vue'
import { handleSevereError } from '@/store/error.js'
const props = defineProps({
nextPage: {
callback: {
type: Function,
required: true,
},
prevPage: {
type: Function,
required: true,
},
modal: {
type: Boolean,
required: true,
},
})
const modal = ref()
const turnstileToken = ref()
const widgetId = ref()
defineExpose({
show: () => {
modal.value.show()
if (window.turnstile === null || !window.turnstile) {
const script = document.createElement('script')
script.src =
'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback'
script.async = true
script.defer = true
document.head.appendChild(script)
window.onloadTurnstileCallback = loadWidget
} else {
loadWidget()
}
},
})
function loadWidget() {
widgetId.value = window.turnstile.render('#turnstile-container', {
sitekey: '0x4AAAAAAAW3guHM6Eunbgwu',
callback: (token) => (turnstileToken.value = token),
expiredCallback: () => (turnstileToken.value = null),
})
}
function removeWidget() {
if (widgetId.value) {
window.turnstile.remove(widgetId.value)
widgetId.value = null
turnstileToken.value = null
}
}
const loggingIn = ref(true)
const twoFactorFlow = ref(null)
const twoFactorCode = ref('')
@@ -45,22 +71,13 @@ const confirmPassword = ref('')
const subscribe = ref(true)
async function signInOauth(provider) {
const url = await authenticate_begin_flow(provider).catch(handleError)
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: url,
},
})
const creds = await authenticate_await_completion().catch(handleError)
const creds = await login(provider).catch(handleSevereError)
if (creds && creds.type === 'two_factor_required') {
twoFactorFlow.value = creds.flow
} else if (creds && creds.session) {
props.nextPage()
props.callback()
modal.value.hide()
}
}
@@ -68,22 +85,22 @@ async function signIn2fa() {
const creds = await login_2fa(twoFactorCode.value, twoFactorFlow.value).catch(handleError)
if (creds && creds.session) {
props.nextPage()
props.callback()
modal.value.hide()
}
}
async function signIn() {
const creds = await login_pass(
username.value,
password.value,
window.turnstile.getResponse(),
).catch(handleError)
window.turnstile.reset()
const creds = await login_pass(username.value, password.value, turnstileToken.value).catch(
handleError,
)
window.turnstile.reset(widgetId.value)
if (creds && creds.type === 'two_factor_required') {
twoFactorFlow.value = creds.flow
} else if (creds && creds.session) {
props.nextPage()
props.callback()
modal.value.hide()
}
}
@@ -102,117 +119,128 @@ async function createAccount() {
username.value,
email.value,
password.value,
window.turnstile.getResponse(),
turnstileToken.value,
subscribe.value,
).catch(handleError)
window.turnstile.reset()
window.turnstile.reset(widgetId.value)
if (creds && creds.session) {
props.nextPage()
props.callback()
modal.value.hide()
}
}
async function goToNextPage() {
props.nextPage()
}
onMounted(() => {
if (window.turnstile === null || !window.turnstile) {
const script = document.createElement('script')
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
script.async = true
script.defer = true
document.head.appendChild(script)
}
})
</script>
<template>
<Card>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAAHWfmKCm7cUG869"></div>
<template v-if="twoFactorFlow">
<h1>Enter two-factor code</h1>
<p>Please enter a two-factor code to proceed.</p>
<input v-model="twoFactorCode" maxlength="11" type="text" placeholder="Enter code..." />
</template>
<template v-else>
<h1 v-if="loggingIn">Login to Modrinth</h1>
<h1 v-else>Create an account</h1>
<div class="button-grid">
<Button class="discord" large @click="signInOauth('discord')">
<DiscordIcon />
Discord
</Button>
<Button class="github" large @click="signInOauth('github')">
<GithubIcon />
Github
</Button>
<Button class="white" large @click="signInOauth('microsoft')">
<MicrosoftIcon />
Microsoft
</Button>
<Button class="google" large @click="signInOauth('google')">
<GoogleIcon />
Google
</Button>
<Button class="white" large @click="signInOauth('steam')">
<SteamIcon />
Steam
</Button>
<Button class="gitlab" large @click="signInOauth('gitlab')">
<GitLabIcon />
GitLab
</Button>
</div>
<div class="divider">
<hr />
<p>Or</p>
</div>
<div v-if="!loggingIn" class="iconified-input username">
<MailIcon />
<input v-model="email" type="text" placeholder="Email" />
</div>
<div class="iconified-input username">
<UserIcon />
<input
v-model="username"
type="text"
:placeholder="loggingIn ? 'Email or username' : 'Username'"
<Modal ref="modal" :on-hide="removeWidget">
<Card>
<template v-if="twoFactorFlow">
<h1>Enter two-factor code</h1>
<p>Please enter a two-factor code to proceed.</p>
<input v-model="twoFactorCode" maxlength="11" type="text" placeholder="Enter code..." />
</template>
<template v-else>
<h1 v-if="loggingIn">Login to Modrinth</h1>
<h1 v-else>Create an account</h1>
<div class="button-grid">
<Button class="discord" large @click="signInOauth('discord')">
<DiscordIcon />
Discord
</Button>
<Button class="github" large @click="signInOauth('github')">
<GithubIcon />
Github
</Button>
<Button class="white" large @click="signInOauth('microsoft')">
<MicrosoftIcon />
Microsoft
</Button>
<Button class="google" large @click="signInOauth('google')">
<GoogleIcon />
Google
</Button>
<Button class="white" large @click="signInOauth('steam')">
<SteamIcon />
Steam
</Button>
<Button class="gitlab" large @click="signInOauth('gitlab')">
<GitLabIcon />
GitLab
</Button>
</div>
<div class="divider">
<hr />
<p>Or</p>
</div>
<div v-if="!loggingIn" class="iconified-input username">
<MailIcon />
<input v-model="email" type="text" placeholder="Email" />
</div>
<div class="iconified-input username">
<UserIcon />
<input
v-model="username"
type="text"
:placeholder="loggingIn ? 'Email or username' : 'Username'"
/>
</div>
<div class="iconified-input" :class="{ username: !loggingIn }">
<LockIcon />
<input v-model="password" type="password" placeholder="Password" />
</div>
<div v-if="!loggingIn" class="iconified-input username">
<LockIcon />
<input v-model="confirmPassword" type="password" placeholder="Confirm password" />
</div>
<div class="turnstile">
<div id="turnstile-container"></div>
<div id="turnstile-container-2"></div>
</div>
<Checkbox
v-if="!loggingIn"
v-model="subscribe"
class="subscribe-btn"
label="Subscribe to updates about Modrinth"
/>
<div class="link-row">
<a v-if="loggingIn" class="button-base" @click="loggingIn = false"> Create account </a>
<a v-else class="button-base" @click="loggingIn = true">Sign in</a>
<a class="button-base" href="https://modrinth.com/auth/reset-password">
Forgot password?
</a>
</div>
</template>
<div class="button-row">
<Button class="transparent" large>Close</Button>
<Button v-if="twoFactorCode" color="primary" large @click="signIn2fa"> Login </Button>
<Button
v-else-if="loggingIn"
color="primary"
large
@click="signIn"
:disabled="!turnstileToken"
>
Login
</Button>
<Button v-else color="primary" large @click="createAccount" :disabled="!turnstileToken">
Create account
</Button>
</div>
<div class="iconified-input" :class="{ username: !loggingIn }">
<LockIcon />
<input v-model="password" type="password" placeholder="Password" />
</div>
<div v-if="!loggingIn" class="iconified-input username">
<LockIcon />
<input v-model="confirmPassword" type="password" placeholder="Confirm password" />
</div>
<Checkbox
v-if="!loggingIn"
v-model="subscribe"
class="subscribe-btn"
label="Subscribe to updates about Modrinth"
/>
<div class="link-row">
<a v-if="loggingIn" class="button-base" @click="loggingIn = false"> Create account </a>
<a v-else class="button-base" @click="loggingIn = true">Sign in</a>
<a class="button-base" href="https://modrinth.com/auth/reset-password">
Forgot password?
</a>
</div>
</template>
<div class="button-row">
<Button class="transparent" large @click="prevPage"> {{ modal ? 'Close' : 'Back' }} </Button>
<Button v-if="twoFactorCode" color="primary" large @click="signIn2fa"> Login </Button>
<Button v-else-if="loggingIn" color="primary" large @click="signIn"> Login </Button>
<Button v-else color="primary" large @click="createAccount"> Create account </Button>
<Button v-if="!modal" class="transparent" large @click="goToNextPage"> Next </Button>
</div>
</Card>
</Card>
</Modal>
</template>
<style scoped lang="scss">
:deep(.modal-container) {
.modal-body {
width: auto;
.content {
background: none;
}
}
}
.card {
width: 25rem;
}
@@ -321,4 +349,19 @@ onMounted(() => {
:deep(.checkbox) {
border: none;
}
.turnstile {
display: flex;
justify-content: center;
overflow: hidden;
border-radius: var(--radius-md);
border: 2px solid var(--color-button-bg);
height: 66px;
margin-top: var(--gap-md);
iframe {
margin: -1px;
min-width: calc(100% + 2px);
}
}
</style>

View File

@@ -34,7 +34,7 @@ const prevPage = () => {
const finishOnboarding = async () => {
mixpanel.track('OnboardingFinish')
const settings = await get()
settings.fully_onboarded = true
settings.onboarded = true
await set(settings)
props.finish()
}

View File

@@ -45,11 +45,3 @@ export async function remove_user(user) {
export async function users() {
return await invoke('plugin:auth|auth_users')
}
// Get a user by UUID
// Prefer to use refresh() instead of this because it will refresh the credentials
// user is UUID
// Returns Credentials (of user)
export async function get_user(user) {
return await invoke('plugin:auth|auth_get_user', { user })
}

View File

@@ -0,0 +1,49 @@
import { invoke } from '@tauri-apps/api/tauri'
export async function get_project(id) {
return await invoke('plugin:cache|get_project', { id })
}
export async function get_project_many(ids) {
return await invoke('plugin:cache|get_project_many', { ids })
}
export async function get_version(id) {
return await invoke('plugin:cache|get_version', { id })
}
export async function get_version_many(ids) {
return await invoke('plugin:cache|get_version_many', { ids })
}
export async function get_user(id) {
return await invoke('plugin:cache|get_user', { id })
}
export async function get_user_many(ids) {
return await invoke('plugin:cache|get_user_many', { ids })
}
export async function get_team(id) {
return await invoke('plugin:cache|get_team', { id })
}
export async function get_team_many(ids) {
return await invoke('plugin:cache|get_team_many', { ids })
}
export async function get_organization(id) {
return await invoke('plugin:cache|get_organization', { id })
}
export async function get_organization_many(ids) {
return await invoke('plugin:cache|get_organization_many', { ids })
}
export async function get_search_results(id) {
return await invoke('plugin:cache|get_search_results', { id })
}
export async function get_search_results_many(ids) {
return await invoke('plugin:cache|get_search_results_many', { ids })
}

View File

@@ -17,7 +17,7 @@
// event.payload is the payload object
console.log(event)
})
Putting that in a script will print any emitted signal from rust
*/
import { listen } from '@tauri-apps/api/event'
@@ -93,15 +93,3 @@ export async function command_listener(callback) {
export async function warning_listener(callback) {
return await listen('warning', (event) => callback(event.payload))
}
/// Payload for the 'offline' event
/*
OfflinePayload {
offline: bool, true or false
}
*/
export async function offline_listener(callback) {
return await listen('offline', (event) => {
return callback(event.payload)
})
}

View File

@@ -14,6 +14,14 @@ JavaVersion {
*/
export async function get_java_versions() {
return await invoke('plugin:jre|get_java_versions')
}
export async function set_java_version(javaVersion) {
return await invoke('plugin:jre|set_java_version', { javaVersion })
}
// Finds all the installation of Java 7, if it exists
// Returns [JavaVersion]
export async function find_filtered_jres(version) {

View File

@@ -6,34 +6,8 @@ export async function get_game_versions() {
return await invoke('plugin:metadata|metadata_get_game_versions')
}
// Gets the fabric versions from daedalus
// Gets the given loader versions from daedalus
// Returns Manifest
export async function get_fabric_versions() {
const c = await invoke('plugin:metadata|metadata_get_fabric_versions')
console.log('Getting fabric versions', c)
return c
}
// Gets the forge versions from daedalus
// Returns Manifest
export async function get_forge_versions() {
const c = await invoke('plugin:metadata|metadata_get_forge_versions')
console.log('Getting forge versions', c)
return c
}
// Gets the quilt versions from daedalus
// Returns Manifest
export async function get_quilt_versions() {
const c = await invoke('plugin:metadata|metadata_get_quilt_versions')
console.log('Getting quilt versions', c)
return c
}
// Gets the neoforge versions from daedalus
// Returns Manifest
export async function get_neoforge_versions() {
const c = await invoke('plugin:metadata|metadata_get_neoforge_versions')
console.log('Getting neoforge versions', c)
return c
export async function get_loader_versions(loader) {
return await invoke('plugin:metadata|metadata_get_loader_versions', { loader })
}

View File

@@ -5,17 +5,10 @@
*/
import { invoke } from '@tauri-apps/api/tauri'
export async function authenticate_begin_flow(provider) {
return await invoke('plugin:mr_auth|authenticate_begin_flow', { provider })
export async function login(provider) {
return await invoke('modrinth_auth_login', { provider })
}
export async function authenticate_await_completion() {
return await invoke('plugin:mr_auth|authenticate_await_completion')
}
export async function cancel_flow() {
return await invoke('plugin:mr_auth|cancel_flow')
}
export async function login_pass(username, password, challenge) {
return await invoke('plugin:mr_auth|login_pass', { username, password, challenge })
}
@@ -34,10 +27,6 @@ export async function create_account(username, email, password, challenge, signU
})
}
export async function refresh() {
return await invoke('plugin:mr_auth|refresh')
}
export async function logout() {
return await invoke('plugin:mr_auth|logout')
}

View File

@@ -21,7 +21,8 @@ export async function install(projectId, versionId, packTitle, iconUrl) {
profile_creator.gameVersion,
profile_creator.modloader,
profile_creator.loaderVersion,
profile_creator.icon,
null,
true,
)
return await invoke('plugin:pack|pack_install', { location, profile })
@@ -39,7 +40,8 @@ export async function install_from_file(path) {
profile_creator.gameVersion,
profile_creator.modloader,
profile_creator.loaderVersion,
profile_creator.icon,
null,
true,
)
return await invoke('plugin:pack|pack_install', { location, profile })
}

View File

@@ -5,49 +5,19 @@
*/
import { invoke } from '@tauri-apps/api/tauri'
/// Gets if a process has finished by UUID
/// Returns bool
export async function has_finished_by_uuid(uuid) {
return await invoke('plugin:process|process_has_finished_by_uuid', { uuid })
}
/// Gets process exit status by UUID
/// Returns u32
export async function get_exit_status_by_uuid(uuid) {
return await invoke('plugin:process|process_get_exit_status_by_uuid', { uuid })
}
/// Gets all process IDs
/// Gets all running process IDs with a given profile path
/// Returns [u32]
export async function get_all_uuids() {
return await invoke('plugin:process|process_get_all_uuids')
}
/// Gets all running process IDs
/// Returns [u32]
export async function get_all_running_uuids() {
return await invoke('plugin:process|process_get_all_running_uuids')
export async function get_by_profile_path(path) {
return await invoke('plugin:process|process_get_by_profile_path', { path })
}
/// Gets all running process IDs with a given profile path
/// Returns [u32]
export async function get_uuids_by_profile_path(profilePath) {
return await invoke('plugin:process|process_get_uuids_by_profile_path', { profilePath })
}
/// Gets all running process IDs with a given profile path
/// Returns [u32]
export async function get_all_running_profile_paths(profilePath) {
return await invoke('plugin:process|process_get_all_running_profile_paths', { profilePath })
}
/// Gets all running process IDs with a given profile path
/// Returns [u32]
export async function get_all_running_profiles() {
return await invoke('plugin:process|process_get_all_running_profiles')
export async function get_all() {
return await invoke('plugin:process|process_get_all')
}
/// Kills a process by UUID
export async function kill_by_uuid(uuid) {
return await invoke('plugin:process|process_kill_by_uuid', { uuid })
export async function kill(pid) {
return await invoke('plugin:process|process_kill', { pid })
}

View File

@@ -16,7 +16,7 @@ import { invoke } from '@tauri-apps/api/tauri'
- icon is a path to an image file, which will be copied into the profile directory
*/
export async function create(name, gameVersion, modloader, loaderVersion, icon, noWatch) {
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
//Trim string name to avoid "Unable to find directory"
name = name.trim()
return await invoke('plugin:profile_create|profile_create', {
@@ -24,8 +24,8 @@ export async function create(name, gameVersion, modloader, loaderVersion, icon,
gameVersion,
modloader,
loaderVersion,
icon,
noWatch,
iconPath,
skipInstall,
})
}
@@ -41,8 +41,18 @@ export async function remove(path) {
// Get a profile by path
// Returns a Profile
export async function get(path, clearProjects) {
return await invoke('plugin:profile|profile_get', { path, clearProjects })
export async function get(path) {
return await invoke('plugin:profile|profile_get', { path })
}
export async function get_many(paths) {
return await invoke('plugin:profile|profile_get_many', { paths })
}
// Get a profile's projects
// Returns a map of a path to profile file
export async function get_projects(path) {
return await invoke('plugin:profile|profile_get_projects', { path })
}
// Get a profile's full fs path
@@ -65,8 +75,8 @@ export async function get_optimal_jre_key(path) {
// Get a copy of the profile set
// Returns hashmap of path -> Profile
export async function list(clearProjects) {
return await invoke('plugin:profile|profile_list', { clearProjects })
export async function list() {
return await invoke('plugin:profile|profile_list')
}
export async function check_installed(path, projectId) {
@@ -163,10 +173,8 @@ export async function run(path) {
return await invoke('plugin:profile|profile_run', { path })
}
// Run Minecraft using a pathed profile
// Waits for end
export async function run_wait(path) {
return await invoke('plugin:profile|profile_run_wait', { path })
export async function kill(path) {
return await invoke('plugin:profile|profile_kill', { path })
}
// Edits a profile

View File

@@ -16,11 +16,6 @@ export async function progress_bars_list() {
return await invoke('plugin:utils|progress_bars_list')
}
// Check if any safe loading bars are active
export async function check_safe_loading_bars_complete() {
return await invoke('plugin:utils|safety_check_safe_loading_bars')
}
// Get opening command
// For example, if a user clicks on an .mrpack to open the app.
// This should be called once and only when the app is done booting up and ready to receive a command
@@ -28,8 +23,3 @@ export async function check_safe_loading_bars_complete() {
export async function get_opening_command() {
return await invoke('plugin:utils|get_opening_command')
}
// Wait for settings to sync
export async function await_sync() {
return await invoke('plugin:utils|await_sync')
}

View File

@@ -5,11 +5,6 @@
*/
import { invoke } from '@tauri-apps/api/tauri'
// Gets tag bundle of all tags
export async function get_tag_bundle() {
return await invoke('plugin:tags|tags_get_tag_bundle')
}
// Gets cached category tags
export async function get_categories() {
return await invoke('plugin:tags|tags_get_categories')

View File

@@ -1,11 +1,4 @@
import {
add_project_from_version as installMod,
check_installed,
get_full_path,
get_mod_full_path,
} from '@/helpers/profile'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
import { invoke } from '@tauri-apps/api/tauri'
export async function isDev() {
@@ -49,55 +42,16 @@ export const releaseColor = (releaseType) => {
}
}
export const installVersionDependencies = async (profile, version) => {
for (const dep of version.dependencies) {
if (dep.dependency_type !== 'required') continue
// disallow fabric api install on quilt
if (dep.project_id === 'P7dR8mSH' && profile.metadata.loader === 'quilt') continue
if (dep.version_id) {
if (
dep.project_id &&
(await check_installed(profile.path, dep.project_id).catch(handleError))
)
continue
await installMod(profile.path, dep.version_id)
} else {
if (
dep.project_id &&
(await check_installed(profile.path, dep.project_id).catch(handleError))
)
continue
const depVersions = await useFetch(
`https://api.modrinth.com/v2/project/${dep.project_id}/version`,
'dependency versions',
)
const latest = depVersions.find(
(v) =>
v.game_versions.includes(profile.metadata.game_version) &&
v.loaders.includes(profile.metadata.loader),
)
if (latest) {
await installMod(profile.path, latest.id).catch(handleError)
}
export function debounce(fn, wait) {
let timer
return function (...args) {
if (timer) {
clearTimeout(timer) // clear any pre-existing timer
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this // get the current context
timer = setTimeout(() => {
fn.apply(context, args) // call the function if time expires
}, wait)
}
}
export const openLink = (url) => {
window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: url,
},
})
}
export const refreshOffline = async () => {
return await invoke('plugin:utils|refresh_offline', {})
}
// returns true/false
export const isOffline = async () => {
return await invoke('plugin:utils|is_offline', {})
}

View File

@@ -10,6 +10,7 @@ import {
NavRow,
Card,
SearchFilter,
Avatar,
} from '@modrinth/ui'
import { formatCategoryHeader, formatCategory } from '@modrinth/utils'
import Multiselect from 'vue-multiselect'
@@ -17,29 +18,22 @@ import { handleError } from '@/store/state'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
import { useRoute, useRouter } from 'vue-router'
import { Avatar } from '@modrinth/ui'
import SearchCard from '@/components/ui/SearchCard.vue'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js'
import { check_installed, get, get as getInstance } from '@/helpers/profile.js'
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { isOffline } from '@/helpers/utils'
import { offline_listener } from '@/helpers/events'
import { get_search_results } from '@/helpers/cache.js'
import { debounce } from '@/helpers/utils.js'
const router = useRouter()
const route = useRoute()
const offline = ref(await isOffline())
const unlistenOffline = await offline_listener((b) => {
offline.value = b
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarningModal = ref(null)
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setContext({ name: 'Browse', link: route.path, query: route.query })
@@ -65,6 +59,7 @@ const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref(route.params.projectType)
const instanceContext = ref(null)
const instanceProjects = ref(null)
const ignoreInstanceLoaders = ref(false)
const ignoreInstanceGameVersions = ref(false)
@@ -88,7 +83,10 @@ if (route.query.il) {
ignoreInstanceLoaders.value = route.query.il === 'true'
}
if (route.query.i) {
instanceContext.value = await getInstance(route.query.i, true)
;[instanceContext.value, instanceProjects.value] = await Promise.all([
getInstance(route.query.i).catch(handleError),
getInstanceProjects(route.query.i).catch(handleError),
])
}
if (route.query.q) {
query.value = route.query.q
@@ -144,18 +142,16 @@ if (route.query.ai) {
}
async function refreshSearch() {
const base = 'https://api.modrinth.com/v2/'
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`]
if (query.value.length > 0) {
params.push(`query=${query.value.replace(/ /g, '+')}`)
}
if (instanceContext.value) {
if (!ignoreInstanceLoaders.value && projectType.value === 'mod') {
orFacets.value = [`categories:${encodeURIComponent(instanceContext.value.metadata.loader)}`]
orFacets.value = [`categories:${encodeURIComponent(instanceContext.value.loader)}`]
}
if (!ignoreInstanceGameVersions.value) {
selectedVersions.value = [instanceContext.value.metadata.game_version]
selectedVersions.value = [instanceContext.value.game_version]
}
}
if (
@@ -224,13 +220,11 @@ async function refreshSearch() {
}
if (hideAlreadyInstalled.value) {
const installedMods = await get(instanceContext.value.path, false).then((x) =>
Object.values(x.projects)
.filter((x) => x.metadata.project)
.map((x) => x.metadata.project.id),
)
const installedMods = Object.values(instanceProjects.value)
.filter((x) => x.metadata)
.map((x) => x.metadata.project_id)
installedMods.map((x) => [`project_id != ${x}`]).forEach((x) => formattedFacets.push(x))
console.log(`facets=${JSON.stringify(formattedFacets)}`)
}
params.push(`facets=${JSON.stringify(formattedFacets)}`)
@@ -246,24 +240,24 @@ async function refreshSearch() {
}
}
let val = `${base}${url}`
let rawResults = await useFetch(val, 'search results', offline.value)
let rawResults = await get_search_results(`?${url}`)
if (!rawResults) {
rawResults = {
hits: [],
total_hits: 0,
limit: 1,
result: {
hits: [],
total_hits: 0,
limit: 1,
},
}
}
if (instanceContext.value) {
for (val of rawResults.hits) {
val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
(x) => (val.installed = x),
for (const val of rawResults.result.hits) {
val.installed = Object.values(instanceProjects.value).some(
(x) => x.metadata && x.metadata.project_id === val.project_id,
)
}
}
results.value = rawResults
results.value = rawResults.result
}
async function onSearchChange(newPageNumber) {
@@ -282,6 +276,8 @@ async function onSearchChange(newPageNumber) {
}
}
const debouncedSearchChange = debounce(() => onSearchChange(1), 200)
const searchWrapper = ref(null)
async function onSearchChangeToTop(newPageNumber) {
await onSearchChange(newPageNumber)
@@ -505,13 +501,13 @@ const selectableProjectTypes = computed(() => {
if (instanceContext.value) {
if (
availableGameVersions.value.findIndex(
(x) => x.version === instanceContext.value.metadata.game_version,
(x) => x.version === instanceContext.value.game_version,
) <= availableGameVersions.value.findIndex((x) => x.version === '1.13')
) {
values.unshift({ label: 'Data Packs', href: `/browse/datapack` })
}
if (instanceContext.value.metadata.loader !== 'vanilla') {
if (instanceContext.value.loader !== 'vanilla') {
values.unshift({ label: 'Mods', href: '/browse/mod' })
}
} else {
@@ -528,8 +524,6 @@ const showVersions = computed(
)
const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.value))
onUnmounted(() => unlistenOffline())
</script>
<template>
@@ -538,27 +532,19 @@ onUnmounted(() => unlistenOffline())
<Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
<Avatar
:src="
!instanceContext.metadata.icon ||
(instanceContext.metadata.icon && instanceContext.metadata.icon.startsWith('http'))
? instanceContext.metadata.icon
: convertFileSrc(instanceContext.metadata.icon)
"
:alt="instanceContext.metadata.name"
:src="instanceContext.icon_path ? convertFileSrc(instanceContext.icon_path) : null"
:alt="instanceContext.name"
size="sm"
/>
<div class="small-instance_info">
<span class="title">{{
instanceContext.metadata.name.length > 20
? instanceContext.metadata.name.substring(0, 20) + '...'
: instanceContext.metadata.name
instanceContext.name.length > 20
? instanceContext.name.substring(0, 20) + '...'
: instanceContext.name
}}</span>
<span>
{{
instanceContext.metadata.loader.charAt(0).toUpperCase() +
instanceContext.metadata.loader.slice(1)
}}
{{ instanceContext.metadata.game_version }}
{{ instanceContext.loader.charAt(0).toUpperCase() + instanceContext.loader.slice(1) }}
{{ instanceContext.game_version }}
</span>
</div>
</router-link>
@@ -598,7 +584,10 @@ onUnmounted(() => unlistenOffline())
>
<ClearIcon /> Clear filters
</Button>
<div v-if="isModProject || projectType === 'shader'" class="loaders">
<div
v-if="(isModProject && ignoreInstanceLoaders) || projectType === 'shader'"
class="loaders"
>
<h2>Loaders</h2>
<div v-for="loader in filteredLoaders" :key="loader">
<SearchFilter
@@ -693,9 +682,10 @@ onUnmounted(() => unlistenOffline())
<input
v-model="query"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="`Search ${projectType}s...`"
@input="onSearchChange(1)"
@input="debouncedSearchChange()"
/>
<Button class="r-btn" @click="() => clearSearch()">
<XIcon />
@@ -752,9 +742,6 @@ onUnmounted(() => unlistenOffline())
loader.supported_project_types?.includes(projectType),
),
]"
:confirm-modal="confirmModal"
:mod-install-modal="modInstallModal"
:incompatibility-warning-modal="incompatibilityWarningModal"
:installed="result.installed"
/>
</section>
@@ -768,9 +755,6 @@ onUnmounted(() => unlistenOffline())
<br />
</div>
</div>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -1,14 +1,13 @@
<script setup>
import { ref, onUnmounted, shallowRef, computed } from 'vue'
import { ref, onUnmounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js'
import { offline_listener, profile_listener } from '@/helpers/events'
import { profile_listener } from '@/helpers/events'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs'
import { isOffline } from '@/helpers/utils'
import { get_search_results } from '@/helpers/cache.js'
const featuredModpacks = ref({})
const featuredMods = ref({})
@@ -19,45 +18,55 @@ const breadcrumbs = useBreadcrumbs()
breadcrumbs.setRootContext({ name: 'Home', link: route.path })
const recentInstances = shallowRef([])
const recentInstances = ref([])
const offline = ref(await isOffline())
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const getInstances = async () => {
const profiles = await list(true).catch(handleError)
recentInstances.value = Object.values(profiles).sort((a, b) => {
return dayjs(b.metadata.last_played ?? 0).diff(dayjs(a.metadata.last_played ?? 0))
const profiles = await list().catch(handleError)
recentInstances.value = profiles.sort((a, b) => {
const dateA = dayjs(a.last_played ?? 0)
const dateB = dayjs(b.last_played ?? 0)
if (dateA.isSame(dateB)) {
return a.name.localeCompare(b.name)
}
return dateB - dateA
})
let filters = []
for (const instance of recentInstances.value) {
if (instance.metadata.linked_data && instance.metadata.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.metadata.linked_data.project_id}"`)
if (instance.linked_data && instance.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
}
}
filter.value = filters.join(' AND ')
}
const getFeaturedModpacks = async () => {
const response = await useFetch(
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
'featured modpacks',
offline.value,
const response = await get_search_results(
`?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
)
if (response) {
featuredModpacks.value = response.hits
featuredModpacks.value = response.result.hits
} else {
featuredModpacks.value = []
}
}
const getFeaturedMods = async () => {
const response = await useFetch(
'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
'featured mods',
offline.value,
)
const response = await get_search_results('?facets=[["project_type:mod"]]&limit=10&index=follows')
if (response) {
featuredMods.value = response.hits
featuredMods.value = response.result.hits
} else {
featuredModpacks.value = []
}
@@ -69,14 +78,8 @@ await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
const unlistenProfile = await profile_listener(async (e) => {
await getInstances()
if (e.event === 'created' || e.event === 'removed') {
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
}
})
const unlistenOffline = await offline_listener(async (b) => {
offline.value = b
if (!b) {
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
}
})
@@ -92,7 +95,6 @@ const total = computed(() => {
onUnmounted(() => {
unlistenProfile()
unlistenOffline()
})
</script>
@@ -105,6 +107,7 @@ onUnmounted(() => {
label: 'Jump back in',
route: '/library',
instances: recentInstances,
instance: true,
downloaded: true,
},
{

View File

@@ -4,34 +4,33 @@ import GridDisplay from '@/components/GridDisplay.vue'
import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { offline_listener, profile_listener } from '@/helpers/events.js'
import { profile_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js'
import { Button } from '@modrinth/ui'
import { PlusIcon } from '@modrinth/assets'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { NewInstanceImage } from '@/assets/icons'
import { isOffline } from '@/helpers/utils'
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
const profiles = await list(true).catch(handleError)
const instances = shallowRef(Object.values(profiles))
const instances = shallowRef(await list().catch(handleError))
const offline = ref(await isOffline())
const unlistenOffline = await offline_listener((b) => {
offline.value = b
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const unlistenProfile = await profile_listener(async () => {
const profiles = await list(true).catch(handleError)
instances.value = Object.values(profiles)
instances.value = await list().catch(handleError)
})
onUnmounted(() => {
unlistenProfile()
unlistenOffline()
})
</script>

View File

@@ -4,7 +4,7 @@ import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, UpdatedIcon } from '@
import { Card, Slider, DropdownSelect, Toggle, Modal, Button } from '@modrinth/ui'
import { handleError, useTheming } from '@/store/state'
import { is_dir_writeable, change_config_dir, get, set } from '@/helpers/settings'
import { get_max_memory } from '@/helpers/jre'
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
@@ -12,6 +12,7 @@ import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/m
import { open } from '@tauri-apps/api/dialog'
import { getOS } from '@/helpers/utils.js'
import { getVersion } from '@tauri-apps/api/app'
import { get_user } from '@/helpers/cache.js'
const pageOptions = ['Home', 'Library']
@@ -22,8 +23,8 @@ const version = await getVersion()
const accessSettings = async () => {
const settings = await get()
settings.javaArgs = settings.custom_java_args.join(' ')
settings.envArgs = settings.custom_env_args.map((x) => x.join('=')).join(' ')
settings.launchArgs = settings.extra_launch_args.join(' ')
settings.envVars = settings.custom_env_vars.map((x) => x.join('=')).join(' ')
return settings
}
@@ -31,7 +32,8 @@ const accessSettings = async () => {
const fetchSettings = await accessSettings().catch(handleError)
const settings = ref(fetchSettings)
const settingsDir = ref(settings.value.loaded_config_dir)
// const settingsDir = ref(settings.value.loaded_config_dir)
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
watch(
@@ -43,26 +45,14 @@ watch(
const setSettings = JSON.parse(JSON.stringify(newSettings))
if (setSettings.opt_out_analytics) {
if (setSettings.telemetry) {
mixpanel_opt_out_tracking()
} else {
mixpanel_opt_in_tracking()
}
for (const [key, value] of Object.entries(setSettings.java_globals)) {
if (value?.path === '') {
value.path = undefined
}
if (value?.path) {
value.path = value.path.replace('java.exe', 'javaw.exe')
}
console.log(`${key}: ${value}`)
}
setSettings.custom_java_args = setSettings.javaArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_args = setSettings.envArgs
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_vars = setSettings.envVars
.trim()
.split(/\s+/)
.filter(Boolean)
@@ -78,22 +68,49 @@ watch(
setSettings.hooks.post_exit = null
}
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
const credentials = ref(await getCreds().catch(handleError))
const javaVersions = ref(await get_java_versions().catch(handleError))
async function updateJavaVersion(version) {
if (version?.path === '') {
version.path = undefined
}
if (version?.path) {
version.path = version.path.replace('java.exe', 'javaw.exe')
}
await set_java_version(version).catch(handleError)
}
async function fetchCredentials() {
const creds = await getCreds().catch(handleError)
console.log(creds)
if (creds && creds.user_id) {
creds.user = await get_user(creds.user_id).catch(handleError)
}
credentials.value = creds
}
const credentials = ref()
await fetchCredentials()
const loginScreenModal = ref()
async function logOut() {
await logout().catch(handleError)
credentials.value = await getCreds().catch(handleError)
await fetchCredentials()
}
async function signInAfter() {
loginScreenModal.value.hide()
credentials.value = await getCreds().catch(handleError)
await fetchCredentials()
}
async function findLauncherDir() {
@@ -103,24 +120,10 @@ async function findLauncherDir() {
title: 'Select a new app directory',
})
const writeable = await is_dir_writeable(newDir)
if (!writeable) {
handleError('The selected directory does not have proper permissions for write access.')
return
}
if (newDir) {
settingsDir.value = newDir
await refreshDir()
settings.value.custom_dir = newDir
}
}
async function refreshDir() {
await change_config_dir(settingsDir.value).catch(handleError)
settings.value = await accessSettings().catch(handleError)
settingsDir.value = settings.value.loaded_config_dir
}
</script>
<template>
@@ -131,13 +134,7 @@ async function refreshDir() {
<span class="label__title size-card-header">General settings</span>
</h3>
</div>
<Modal
ref="loginScreenModal"
class="login-screen-modal"
:noblur="!themeStore.advancedRendering"
>
<ModrinthLoginScreen :modal="true" :prev-page="signInAfter" :next-page="signInAfter" />
</Modal>
<ModrinthLoginScreen ref="loginScreenModal" :callback="signInAfter" />
<div class="adjacent-input">
<label for="theme">
<span class="label__title">Manage account</span>
@@ -164,15 +161,11 @@ async function refreshDir() {
<div class="app-directory">
<div class="iconified-input">
<BoxIcon />
<input id="appDir" v-model="settingsDir" type="text" class="input" />
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
<Button class="r-btn" @click="findLauncherDir">
<FolderSearchIcon />
</Button>
</div>
<Button large @click="refreshDir">
<UpdatedIcon />
Refresh
</Button>
</div>
</Card>
<Card>
@@ -230,11 +223,11 @@ async function refreshDir() {
</label>
<Toggle
id="minimize-launcher"
:model-value="settings.hide_on_process"
:checked="settings.hide_on_process"
:model-value="settings.hide_on_process_start"
:checked="settings.hide_on_process_start"
@update:model-value="
(e) => {
settings.hide_on_process = e
settings.hide_on_process_start = e
}
"
/>
@@ -285,10 +278,11 @@ async function refreshDir() {
<div class="adjacent-input">
<label for="max-downloads">
<span class="label__title">Maximum concurrent downloads</span>
<span class="label__description"
>The maximum amount of files the launcher can download at the same time. Set this to a
lower value if you have a poor internet connection.</span
>
<span class="label__description">
The maximum amount of files the launcher can download at the same time. Set this to a
lower value if you have a poor internet connection. (app restart required to take
effect)
</span>
</label>
<Slider
id="max-downloads"
@@ -302,10 +296,11 @@ async function refreshDir() {
<div class="adjacent-input">
<label for="max-writes">
<span class="label__title">Maximum concurrent writes</span>
<span class="label__description"
>The maximum amount of files the launcher can write to the disk at once. Set this to a
lower value if you are frequently getting I/O errors.</span
>
<span class="label__description">
The maximum amount of files the launcher can write to the disk at once. Set this to a
lower value if you are frequently getting I/O errors. (app restart required to take
effect)
</span>
</label>
<Slider
id="max-writes"
@@ -324,37 +319,38 @@ async function refreshDir() {
</div>
<div class="adjacent-input">
<label for="opt-out-analytics">
<span class="label__title">Disable analytics</span>
<span class="label__title">Telemetry</span>
<span class="label__description">
Modrinth collects anonymized analytics and usage data to improve our user experience and
customize your experience. By enabling this option, you opt out and your data will no
customize your experience. By disabling this option, you opt out and your data will no
longer be collected.
</span>
</label>
<Toggle
id="opt-out-analytics"
:model-value="settings.opt_out_analytics"
:checked="settings.opt_out_analytics"
:model-value="settings.telemetry"
:checked="settings.telemetry"
@update:model-value="
(e) => {
settings.opt_out_analytics = e
settings.telemetry = e
}
"
/>
</div>
<div class="adjacent-input">
<label for="disable-discord-rpc">
<span class="label__title">Disable Discord RPC</span>
<span class="label__title">Discord RPC</span>
<span class="label__description">
Disables the Discord Rich Presence integration. 'Modrinth' will no longer show up as a
game or app you are using on your Discord profile. This does not disable any
instance-specific Discord Rich Presence integrations, such as those added by mods.
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to
no longer show up as a game or app you are using on your Discord profile. This does not
disable any instance-specific Discord Rich Presence integrations, such as those added by
mods. (app restart required to take effect)
</span>
</label>
<Toggle
id="disable-discord-rpc"
v-model="settings.disable_discord_rpc"
:checked="settings.disable_discord_rpc"
v-model="settings.discord_rpc"
:checked="settings.discord_rpc"
/>
</div>
</Card>
@@ -364,25 +360,24 @@ async function refreshDir() {
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<label for="java-21">
<span class="label__title">Java 21 location</span>
</label>
<JavaSelector id="java-17" v-model="settings.java_globals.JAVA_21" :version="21" />
<label for="java-17">
<span class="label__title">Java 17 location</span>
</label>
<JavaSelector id="java-17" v-model="settings.java_globals.JAVA_17" :version="17" />
<label for="java-8">
<span class="label__title">Java 8 location</span>
</label>
<JavaSelector id="java-8" v-model="settings.java_globals.JAVA_8" :version="8" />
<template v-for="version in [21, 17, 8]">
<label :for="'java-' + version">
<span class="label__title">Java {{ version }} location</span>
</label>
<JavaSelector
:id="'java-selector-' + version"
v-model="javaVersions[version]"
:version="version"
@update:model-value="updateJavaVersion"
/>
</template>
<hr class="card-divider" />
<label for="java-args">
<span class="label__title">Java arguments</span>
</label>
<input
id="java-args"
v-model="settings.javaArgs"
v-model="settings.launchArgs"
autocomplete="off"
type="text"
class="installation-input"
@@ -393,7 +388,7 @@ async function refreshDir() {
</label>
<input
id="env-vars"
v-model="settings.envArgs"
v-model="settings.envVars"
autocomplete="off"
type="text"
class="installation-input"
@@ -526,7 +521,7 @@ async function refreshDir() {
<div>
<label>
<span class="label__title">App version</span>
<span class="label__description">Theseus v{{ version }} </span>
<span class="label__description">Modrinth App v{{ version }} </span>
</label>
</div>
</Card>
@@ -551,16 +546,6 @@ async function refreshDir() {
margin: 1rem 0;
}
:deep(.login-screen-modal) {
.modal-container .modal-body {
width: auto;
.content {
background: none;
}
}
}
.app-directory {
display: flex;
flex-direction: row;

View File

@@ -2,20 +2,10 @@
<div class="instance-container">
<div class="side-cards">
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar
size="lg"
:src="
!instance.metadata.icon ||
(instance.metadata.icon && instance.metadata.icon.startsWith('http'))
? instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
/>
<Avatar size="lg" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
<div class="instance-info">
<h2 class="name">{{ instance.metadata.name }}</h2>
<span class="metadata">
{{ instance.metadata.loader }} {{ instance.metadata.game_version }}
</span>
<h2 class="name">{{ instance.name }}</h2>
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
</div>
<span class="button-group">
<Button v-if="instance.install_stage !== 'installed'" disabled class="instance-button">
@@ -26,7 +16,6 @@
color="danger"
class="instance-button"
@click="stopInstance('InstancePage')"
@mouseover="checkProcess"
>
<StopCircleIcon />
Stop
@@ -36,7 +25,6 @@
color="primary"
class="instance-button"
@click="startInstance('InstancePage')"
@mouseover="checkProcess"
>
<PlayIcon />
Play
@@ -135,22 +123,20 @@ import {
CheckCircleIcon,
UpdatedIcon,
} from '@modrinth/assets'
import { get, run } from '@/helpers/profile'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
kill_by_uuid,
} from '@/helpers/process'
import { offline_listener, process_listener, profile_listener } from '@/helpers/events'
import { get, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process'
import { process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router'
import { ref, onUnmounted } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { isOffline, showProfileInFolder } from '@/helpers/utils.js'
import { showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { useFetch } from '@/helpers/fetch'
import { handleSevereError } from '@/store/error.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import dayjs from 'dayjs'
const route = useRoute()
@@ -161,72 +147,71 @@ const instance = ref(await get(route.params.id).catch(handleError))
breadcrumbs.setName(
'Instance',
instance.value.metadata.name.length > 40
? instance.value.metadata.name.substring(0, 40) + '...'
: instance.value.metadata.name,
instance.value.name.length > 40
? instance.value.name.substring(0, 40) + '...'
: instance.value.name,
)
breadcrumbs.setContext({
name: instance.value.metadata.name,
name: instance.value.name,
link: route.path,
query: route.query,
})
const offline = ref(await isOffline())
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const loadingBar = useLoading()
const uuid = ref(null)
const playing = ref(false)
const loading = ref(false)
const options = ref(null)
const startInstance = async (context) => {
loading.value = true
uuid.value = await run(route.params.id).catch(handleSevereError)
run(route.params.id).catch(handleSevereError)
loading.value = false
playing.value = true
mixpanel_track('InstanceStart', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
})
}
const checkProcess = async () => {
const runningPaths = await get_all_running_profile_paths().catch(handleError)
if (runningPaths.includes(instance.value.path)) {
playing.value = true
return
}
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
playing.value = false
uuid.value = null
playing.value = runningProcesses.length > 0
}
// Get information on associated modrinth versions, if any
const modrinthVersions = ref([])
if (!(await isOffline()) && instance.value.metadata.linked_data?.project_id) {
modrinthVersions.value = await useFetch(
`https://api.modrinth.com/v2/project/${instance.value.metadata.linked_data.project_id}/version`,
'project',
)
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
const project = await get_project(instance.value.linked_data.project_id).catch(handleError)
if (project && project.versions) {
modrinthVersions.value = (await get_version_many(project.versions).catch(handleError)).sort(
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
)
}
}
await checkProcess()
const stopInstance = async (context) => {
playing.value = false
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(instance.value.path).catch(handleError)
uuid.value = uuids[0] // populate Uuid to listen for in the process_listener
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError)
await kill(route.params.id).catch(handleError)
mixpanel_track('InstanceStop', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
loader: instance.value.loader,
game_version: instance.value.game_version,
source: context,
})
}
@@ -271,7 +256,7 @@ const handleOptionsClick = async (args) => {
break
case 'add_content':
await router.push({
path: `/browse/${instance.value.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
path: `/browse/${instance.value.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: route.params.id },
})
break
@@ -302,17 +287,12 @@ const unlistenProfiles = await profile_listener(async (event) => {
})
const unlistenProcesses = await process_listener((e) => {
if (e.event === 'finished' && uuid.value === e.uuid) playing.value = false
})
const unlistenOffline = await offline_listener((b) => {
offline.value = b
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
})
onUnmounted(() => {
unlistenProcesses()
unlistenProfiles()
unlistenOffline()
})
</script>

View File

@@ -99,7 +99,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch
import dayjs from 'dayjs'
import isToday from 'dayjs/plugin/isToday'
import isYesterday from 'dayjs/plugin/isYesterday'
import { get_uuids_by_profile_path } from '@/helpers/process.js'
import { get_by_profile_path } from '@/helpers/process.js'
import { useRoute } from 'vue-router'
import { process_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js'
@@ -209,9 +209,9 @@ const processedLogs = computed(() => {
async function getLiveStdLog() {
if (route.params.id) {
const uuids = await get_uuids_by_profile_path(route.params.id).catch(handleError)
const processes = await get_by_profile_path(route.params.id).catch(handleError)
let returnValue
if (uuids.length === 0) {
if (processes.length === 0) {
returnValue = emptyText.join('\n')
} else {
const logCursor = await get_latest_log_cursor(

View File

@@ -281,7 +281,7 @@
<div class="markdown-body">
<p>
Are you sure you want to remove
<strong>{{ functionValues.length }} project(s)</strong> from {{ instance.metadata.name }}?
<strong>{{ functionValues.length }} project(s)</strong> from {{ instance.name }}?
<br />
This action <strong>cannot</strong> be undone.
</p>
@@ -304,7 +304,7 @@
>{{ Array.from(projects.values()).filter((x) => x.disabled).length }} disabled
project(s)</strong
>
from {{ instance.metadata.name }}?
from {{ instance.name }}?
<br />
This action <strong>cannot</strong> be undone.
</p>
@@ -326,7 +326,7 @@
/>
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
<ModpackVersionModal
v-if="instance.metadata.linked_data"
v-if="instance.linked_data"
ref="modpackVersionModal"
:instance="instance"
:versions="props.versions"
@@ -364,6 +364,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'
import {
add_project_from_path,
get,
get_projects,
remove_project,
toggle_disable_project,
update_all,
@@ -372,12 +373,18 @@ import {
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import { listen } from '@tauri-apps/api/event'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { highlightModInProfile } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
import ExportModal from '@/components/ui/ExportModal.vue'
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import AddContentButton from '@/components/ui/AddContentButton.vue'
import {
get_organization_many,
get_project_many,
get_team_many,
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
const props = defineProps({
instance: {
@@ -404,65 +411,102 @@ const props = defineProps({
},
})
const projects = ref([])
const selectionMap = ref(new Map())
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()
}
})
onUnmounted(() => {
unlistenProfiles()
})
const showingOptions = ref(false)
const isPackLocked = computed(() => {
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
return props.instance.linked_data && props.instance.linked_data.locked
})
const canUpdatePack = computed(() => {
if (!props.instance.metadata.linked_data) return false
return props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
if (!props.instance.linked_data || !props.versions || !props.versions[0]) return false
return props.instance.linked_data.version_id !== props.versions[0].id
})
const exportModal = ref(null)
const initProjects = (initInstance) => {
projects.value = []
if (!initInstance || !initInstance.projects) return
for (const [path, project] of Object.entries(initInstance.projects)) {
if (project.metadata.type === 'modrinth' && !props.offline) {
let owner = project.metadata.members.find((x) => x.role === 'Owner')
projects.value.push({
const projects = ref([])
const selectionMap = ref(new Map())
const initProjects = async () => {
const newProjects = []
const profileProjects = await get_projects(props.instance.path)
const fetchProjects = []
const fetchVersions = []
for (const value of Object.values(profileProjects)) {
if (value.metadata) {
fetchProjects.push(value.metadata.project_id)
fetchVersions.push(value.metadata.version_id)
}
}
const [modrinthProjects, modrinthVersions] = await Promise.all([
await get_project_many(fetchProjects).catch(handleError),
await get_version_many(fetchVersions).catch(handleError),
])
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError),
await get_organization_many(
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
).catch(handleError),
])
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 org = project.organization
? modrinthOrganizations.find((x) => x.id === project.organization)
: null
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
let owner = org ? org.name : team.find((x) => x.is_owner).user.username
newProjects.push({
path,
name: project.metadata.project.title,
slug: project.metadata.project.slug,
author: owner ? owner.user.username : null,
version: project.metadata.version.version_number,
file_name: project.file_name,
icon: project.metadata.project.icon_url,
disabled: project.disabled,
updateVersion: project.metadata.update_version,
outdated: !!project.metadata.update_version,
project_type: project.metadata.project.project_type,
id: project.metadata.project.id,
})
} else if (project.metadata.type === 'inferred') {
projects.value.push({
path,
name: project.metadata.title ?? project.file_name,
author: project.metadata.authors[0],
version: project.metadata.version,
file_name: project.file_name,
icon: project.metadata.icon ? convertFileSrc(project.metadata.icon) : null,
disabled: project.disabled,
outdated: false,
project_type: project.metadata.project_type,
name: project.title,
slug: project.slug,
author: owner,
version: version.version_number,
file_name: file.file_name,
icon: project.icon_url,
disabled: file.file_name.endsWith('.disabled'),
updateVersion: file.update_version_id,
outdated: !!file.update_version_id,
project_type: project.project_type,
id: project.id,
})
} else {
projects.value.push({
newProjects.push({
path,
name: project.file_name,
name: file.file_name.replace('.disabled', ''),
author: '',
version: null,
file_name: project.file_name,
file_name: file.file_name,
icon: null,
disabled: project.disabled,
disabled: file.file_name.endsWith('.disabled'),
outdated: false,
project_type: null,
project_type: file.project_type,
})
}
}
projects.value = newProjects
const newSelectionMap = new Map()
for (const project of projects.value) {
newSelectionMap.set(
@@ -475,22 +519,7 @@ const initProjects = (initInstance) => {
}
selectionMap.value = newSelectionMap
}
initProjects(props.instance)
watch(
() => props.instance.projects,
() => {
initProjects(props.instance)
},
)
watch(
() => props.offline,
() => {
if (props.instance) initProjects(props.instance)
},
)
await initProjects()
const modpackVersionModal = ref(null)
const installing = computed(() => props.instance.install_stage !== 'installed')
@@ -633,8 +662,8 @@ const updateAll = async () => {
}
mixpanel_track('InstanceUpdateAll', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
count: setProjects.length,
selected: selected.value.length > 1,
})
@@ -659,8 +688,8 @@ const updateProject = async (mod) => {
mod.updateVersion = null
mixpanel_track('InstanceProjectUpdate', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
name: mod.name,
project_type: mod.project_type,
@@ -686,8 +715,8 @@ const toggleDisableMod = async (mod) => {
mod.path = newPath
mod.disabled = !mod.disabled
mixpanel_track('InstanceProjectDisable', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
name: mod.name,
project_type: mod.project_type,
@@ -707,8 +736,8 @@ const removeMod = async (mod) => {
projects.value = projects.value.filter((x) => mod.path !== x.path)
mixpanel_track('InstanceProjectRemove', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.id,
name: mod.name,
project_type: mod.project_type,
@@ -820,7 +849,7 @@ watch(selectAll, () => {
const unlisten = await listen('tauri://file-drop', async (event) => {
for (const file of event.payload) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file, 'mod').catch(handleError)
await add_project_from_path(props.instance.path, file).catch(handleError)
}
initProjects(await get(props.instance.path).catch(handleError))
})

View File

@@ -25,7 +25,7 @@
<XIcon />
Cancel
</button>
<button class="btn btn-danger" :disabled="action_disabled" @click="unlockProfile">
<button class="btn btn-danger" @click="unlockProfile">
<LockIcon />
Unlock
</button>
@@ -50,7 +50,7 @@
<XIcon />
Cancel
</button>
<button class="btn btn-danger" :disabled="action_disabled" @click="unpairProfile">
<button class="btn btn-danger" @click="unpairProfile">
<XIcon />
Unpair
</button>
@@ -117,21 +117,13 @@
<span class="label__title">Icon</span>
</label>
<div class="input-group">
<Avatar
:src="!icon || (icon && icon.startsWith('http')) ? icon : convertFileSrc(icon)"
size="md"
class="project__icon"
/>
<Avatar :src="icon ? convertFileSrc(icon) : icon" size="md" class="project__icon" />
<div class="input-stack">
<button id="instance-icon" class="btn" @click="setIcon">
<UploadIcon />
Select icon
</button>
<button
:disabled="!(!icon || (icon && icon.startsWith('http')) ? icon : convertFileSrc(icon))"
class="btn"
@click="resetIcon"
>
<button :disabled="!icon" class="btn" @click="resetIcon">
<TrashIcon />
Remove icon
</button>
@@ -147,7 +139,7 @@
autocomplete="off"
maxlength="80"
type="text"
:disabled="instance.metadata.linked_data"
:disabled="instance.linked_data"
/>
<div class="adjacent-input">
@@ -358,7 +350,7 @@
/>
</div>
</Card>
<Card v-if="instance.metadata.linked_data">
<Card v-if="instance.linked_data">
<div class="label">
<h3>
<span class="label__title size-card-header">Modpack</span>
@@ -366,9 +358,7 @@
</div>
<div class="adjacent-input">
<label for="general-modpack-info">
<span class="label__description">
<strong>Modpack: </strong> {{ instance.metadata.name }}
</span>
<span class="label__description"> <strong>Modpack: </strong> {{ instance.name }} </span>
<span class="label__description">
<strong>Version: </strong>
{{
@@ -414,7 +404,7 @@
</Button>
</div>
<div v-if="props.instance.metadata.linked_data.project_id" class="adjacent-input">
<div v-if="instance.linked_data.project_id" class="adjacent-input">
<label for="change-modpack-version">
<span class="label__title">Change modpack version</span>
<span class="label__description">
@@ -502,7 +492,7 @@
</div>
</Card>
<ModpackVersionModal
v-if="instance.metadata.linked_data"
v-if="instance.linked_data"
ref="modpackVersionModal"
:instance="instance"
:versions="props.versions"
@@ -553,12 +543,7 @@ import { get } from '@/helpers/settings.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/dialog'
import {
get_fabric_versions,
get_forge_versions,
get_neoforge_versions,
get_quilt_versions,
} from '@/helpers/metadata.js'
import { get_loader_versions } from '@/helpers/metadata.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel'
@@ -587,17 +572,17 @@ const props = defineProps({
const themeStore = useTheming()
const title = ref(props.instance.metadata.name)
const icon = ref(props.instance.metadata.icon)
const groups = ref(props.instance.metadata.groups)
const title = ref(props.instance.name)
const icon = ref(props.instance.icon_path)
const groups = ref(props.instance.groups)
const modpackVersionModal = ref(null)
const instancesList = Object.values(await list(true))
const instancesList = await list()
const availableGroups = ref([
...new Set(
instancesList.reduce((acc, obj) => {
return acc.concat(obj.metadata.groups)
return acc.concat(obj.groups)
}, []),
),
])
@@ -632,18 +617,18 @@ const globalSettings = await get().catch(handleError)
const modalConfirmUnlock = ref(null)
const modalConfirmUnpair = ref(null)
const javaSettings = props.instance.java ?? {}
const overrideJavaInstall = ref(!!javaSettings.override_version)
const overrideJavaInstall = ref(!!props.instance.java_path)
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
const javaInstall = ref(optimalJava ?? javaSettings.override_version ?? { path: '', version: '' })
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
const overrideJavaArgs = ref(!!javaSettings.extra_arguments)
const javaArgs = ref((javaSettings.extra_arguments ?? globalSettings.custom_java_args).join(' '))
const overrideJavaArgs = ref(!!props.instance.extra_launch_args)
const javaArgs = ref(
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
)
const overrideEnvVars = ref(!!javaSettings.custom_env_args)
const overrideEnvVars = ref(!!props.instance.custom_env_vars)
const envVars = ref(
(javaSettings.custom_env_args ?? globalSettings.custom_env_args)
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
.map((x) => x.join('='))
.join(' '),
)
@@ -652,18 +637,22 @@ const overrideMemorySettings = ref(!!props.instance.memory)
const memory = ref(props.instance.memory ?? globalSettings.memory)
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
const overrideWindowSettings = ref(!!props.instance.resolution || !!props.instance.fullscreen)
const resolution = ref(props.instance.resolution ?? globalSettings.game_resolution)
const overrideHooks = ref(!!props.instance.hooks)
const overrideWindowSettings = ref(
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
)
const resolution = ref(props.instance.game_resolution ?? globalSettings.game_resolution)
const overrideHooks = ref(
props.instance.hooks.pre_launch || props.instance.hooks.wrapper || props.instance.hooks.post_exit,
)
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
const fullscreenSetting = ref(!!props.instance.fullscreen)
const fullscreenSetting = ref(!!props.instance.force_fullscreen)
const unlinkModpack = ref(false)
const inProgress = ref(false)
const installing = computed(() => props.instance.install_stage !== 'installed')
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.version_id)
const installedVersion = computed(() => props.instance?.linked_data?.version_id)
const installedVersionData = computed(() => {
if (!installedVersion.value) return null
return props.versions.find((version) => version.id === installedVersion.value)
@@ -706,34 +695,29 @@ const getLocalVersion = (path) => {
const editProfileObject = computed(() => {
const editProfile = {
metadata: {
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
loader_version: props.instance.metadata.loader_version,
linked_data: props.instance.metadata.linked_data,
},
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
loader_version: props.instance.loader_version,
linked_data: props.instance.linked_data,
java: {},
hooks: {},
}
if (overrideJavaInstall.value) {
if (javaInstall.value.path !== '') {
editProfile.java.override_version = javaInstall.value
editProfile.java.override_version.path = editProfile.java.override_version.path.replace(
'java.exe',
'javaw.exe',
)
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
}
}
if (overrideJavaArgs.value) {
if (javaArgs.value !== '') {
editProfile.java.extra_arguments = javaArgs.value.trim().split(/\s+/).filter(Boolean)
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
}
}
if (overrideEnvVars.value) {
if (envVars.value !== '') {
editProfile.java.custom_env_args = envVars.value
editProfile.custom_env_vars = envVars.value
.trim()
.split(/\s+/)
.filter(Boolean)
@@ -746,10 +730,10 @@ const editProfileObject = computed(() => {
}
if (overrideWindowSettings.value) {
editProfile.fullscreen = fullscreenSetting.value
editProfile.force_fullscreen = fullscreenSetting.value
if (!fullscreenSetting.value) {
editProfile.resolution = resolution.value
editProfile.game_resolution = resolution.value
}
}
@@ -758,10 +742,10 @@ const editProfileObject = computed(() => {
}
if (unlinkModpack.value) {
editProfile.metadata.linked_data = null
editProfile.linked_data = null
}
breadcrumbs.setName('Instance', editProfile.metadata.name)
breadcrumbs.setName('Instance', editProfile.name)
return editProfile
})
@@ -771,8 +755,8 @@ const repairing = ref(false)
async function duplicateProfile() {
await duplicate(props.instance.path).catch(handleError)
mixpanel_track('InstanceDuplicate', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
@@ -782,14 +766,14 @@ async function repairProfile(force) {
repairing.value = false
mixpanel_track('InstanceRepair', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
async function unpairProfile() {
const editProfile = props.instance
editProfile.metadata.linked_data = null
editProfile.linked_data = null
await edit(props.instance.path, editProfile)
installedVersion.value = null
installedVersionData.value = null
@@ -798,13 +782,13 @@ async function unpairProfile() {
async function unlockProfile() {
const editProfile = props.instance
editProfile.metadata.linked_data.locked = false
editProfile.linked_data.locked = false
await edit(props.instance.path, editProfile)
modalConfirmUnlock.value.hide()
}
const isPackLocked = computed(() => {
return props.instance.metadata.linked_data && props.instance.metadata.linked_data.locked
return props.instance.linked_data && props.instance.linked_data.locked
})
async function repairModpack() {
@@ -813,8 +797,8 @@ async function repairModpack() {
inProgress.value = false
mixpanel_track('InstanceRepair', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
}
@@ -825,8 +809,8 @@ async function removeProfile() {
removing.value = false
mixpanel_track('InstanceRemove', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,
loader: props.instance.loader,
game_version: props.instance.game_version,
})
await router.push({ path: '/' })
@@ -843,10 +827,10 @@ const [
all_game_versions,
loaders,
] = await Promise.all([
get_fabric_versions().then(shallowRef).catch(handleError),
get_forge_versions().then(shallowRef).catch(handleError),
get_quilt_versions().then(shallowRef).catch(handleError),
get_neoforge_versions().then(shallowRef).catch(handleError),
get_loader_versions('fabric').then(shallowRef).catch(handleError),
get_loader_versions('forge').then(shallowRef).catch(handleError),
get_loader_versions('quilt').then(shallowRef).catch(handleError),
get_loader_versions('neo').then(shallowRef).catch(handleError),
get_game_versions().then(shallowRef).catch(handleError),
get_loaders()
.then((value) =>
@@ -859,8 +843,8 @@ const [
])
loaders.value.unshift('vanilla')
const loader = ref(props.instance.metadata.loader)
const gameVersion = ref(props.instance.metadata.game_version)
const loader = ref(props.instance.loader)
const gameVersion = ref(props.instance.game_version)
const selectableGameVersions = computed(() => {
return all_game_versions.value
.filter((item) => {
@@ -896,9 +880,7 @@ const selectableLoaderVersions = computed(() => {
return []
})
const loaderVersionIndex = ref(
selectableLoaderVersions.value.findIndex(
(x) => x.id === props.instance.metadata.loader_version?.id,
),
selectableLoaderVersions.value.findIndex((x) => x.id === props.instance.loader_version),
)
const isValid = computed(() => {
@@ -910,10 +892,9 @@ const isValid = computed(() => {
const isChanged = computed(() => {
return (
loader.value != props.instance.metadata.loader ||
gameVersion.value != props.instance.metadata.game_version ||
JSON.stringify(selectableLoaderVersions.value[loaderVersionIndex.value]) !==
JSON.stringify(props.instance.metadata.loader_version)
loader.value !== props.instance.loader ||
gameVersion.value !== props.instance.game_version ||
selectableLoaderVersions.value[loaderVersionIndex.value].id !== props.instance.loader_version
)
})
@@ -924,11 +905,11 @@ async function saveGvLoaderEdits() {
editing.value = true
let editProfile = editProfileObject.value
editProfile.metadata.loader = loader.value
editProfile.metadata.game_version = gameVersion.value
editProfile.loader = loader.value
editProfile.game_version = gameVersion.value
if (loader.value !== 'vanilla') {
editProfile.metadata.loader_version = selectableLoaderVersions.value[loaderVersionIndex.value]
editProfile.loader_version = selectableLoaderVersions.value[loaderVersionIndex.value].id
}
await edit(props.instance.path, editProfile).catch(handleError)
await repairProfile(false)

View File

@@ -4,26 +4,17 @@
<Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar
:src="
!instance.metadata.icon ||
(instance.metadata.icon && instance.metadata.icon.startsWith('http'))
? instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
:alt="instance.metadata.name"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:alt="instance.name"
size="sm"
/>
<div class="small-instance_info">
<span class="title">{{
instance.metadata.name.length > 20
? instance.metadata.name.substring(0, 20) + '...'
: instance.metadata.name
instance.name.length > 20 ? instance.name.substring(0, 20) + '...' : instance.name
}}</span>
<span>
{{
instance.metadata.loader.charAt(0).toUpperCase() + instance.metadata.loader.slice(1)
}}
{{ instance.metadata.game_version }}
{{ instance.loader.charAt(0).toUpperCase() + instance.loader.slice(1) }}
{{ instance.game_version }}
</span>
</div>
</router-link>
@@ -209,7 +200,6 @@
:project="data"
:versions="versions"
:members="members"
:dependencies="dependencies"
:instance="instance"
:install="install"
:installed="installed"
@@ -218,9 +208,6 @@
/>
</div>
</div>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarning" />
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #install> <DownloadIcon /> Install </template>
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
@@ -263,79 +250,63 @@ import {
OpenCollectiveIcon,
} from '@/assets/external'
import { get_categories } from '@/helpers/tags'
import { install as packInstall } from '@/helpers/pack'
import {
list,
add_project_from_version as installMod,
check_installed,
get as getInstance,
remove_project,
} from '@/helpers/profile'
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 { ref, shallowRef, watch } from 'vue'
import { installVersionDependencies, isOffline } from '@/helpers/utils'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { mixpanel_track } from '@/helpers/mixpanel'
import { install as installVersion } from '@/store/install.js'
import { get_project, get_project_many, get_team, get_version_many } from '@/helpers/cache.js'
dayjs.extend(relativeTime)
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarning = ref(null)
const options = ref(null)
const installing = ref(false)
const data = shallowRef(null)
const versions = shallowRef([])
const members = shallowRef([])
const dependencies = shallowRef([])
const categories = shallowRef([])
const instance = ref(null)
const instanceProjects = ref(null)
const installed = ref(false)
const installedVersion = ref(null)
const offline = ref(await isOffline())
async function fetchProjectData() {
;[
data.value,
versions.value,
members.value,
dependencies.value,
categories.value,
instance.value,
] = await Promise.all([
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}`, 'project'),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/version`, 'project'),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/members`, 'project'),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/dependencies`, 'project'),
get_categories().catch(handleError),
route.query.i ? getInstance(route.query.i, false).catch(handleError) : Promise.resolve(),
])
const project = await get_project(route.params.id).catch(handleError)
installed.value =
instance.value?.path &&
(await check_installed(instance.value.path, data.value.id).catch(handleError))
data.value = project
;[versions.value, members.value, categories.value, instance.value, instanceProjects.value] =
await Promise.all([
get_version_many(project.versions).catch(handleError),
get_team(project.team).catch(handleError),
get_categories().catch(handleError),
route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(),
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
])
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
if (instanceProjects.value) {
const installedFile = Object.values(instanceProjects.value).find(
(x) => x.metadata && x.metadata.project_id === data.value.id,
)
if (installedFile) {
installed.value = true
installedVersion.value = installedFile.metadata.version_id
}
}
breadcrumbs.setName('Project', data.value.title)
installedVersion.value = instance.value
? Object.values(instance.value.projects).find(
(p) => p?.metadata?.version?.project_id === data.value.id,
)?.metadata?.version?.id
: null
}
if (!offline.value) await fetchProjectData()
await fetchProjectData()
watch(
() => route.params.id,
@@ -346,162 +317,22 @@ watch(
},
)
dayjs.extend(relativeTime)
const markInstalled = () => {
installed.value = true
}
async function install(version) {
installing.value = true
let queuedVersionData
if (instance.value) {
instance.value = await getInstance(instance.value.path, false).catch(handleError)
}
if (installed.value) {
const old_project = Object.entries(instance.value.projects)
.map(([key, value]) => ({
key,
value,
}))
.find((p) => p.value.metadata?.version?.project_id === data.value.id)
if (!old_project) {
// Switching too fast, old project is not recognized as a Modrinth project yet
await installVersion(
data.value.id,
version,
instance.value ? instance.value.path : null,
'ProjectPage',
(version) => {
installing.value = false
return
}
await remove_project(instance.value.path, old_project.key)
}
if (version) {
queuedVersionData = versions.value.find((v) => v.id === version)
} else {
if (data.value.project_type === 'modpack' || !instance.value) {
queuedVersionData = versions.value[0]
} else {
queuedVersionData = versions.value.find((v) =>
v.game_versions.includes(data.value.game_versions[0]),
)
}
}
if (data.value.project_type === 'modpack') {
const packs = Object.values(await list(true).catch(handleError))
if (
packs.length === 0 ||
!packs
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === data.value.id)
) {
await packInstall(
data.value.id,
queuedVersionData.id,
data.value.title,
data.value.icon_url,
).catch(handleError)
mixpanel_track('PackInstall', {
id: data.value.id,
version_id: queuedVersionData.id,
title: data.value.title,
source: 'ProjectPage',
})
} else {
confirmModal.value.show(
data.value.id,
queuedVersionData.id,
data.value.title,
data.value.icon_url,
)
}
} else {
if (instance.value) {
if (!version) {
const gameVersion = instance.value.metadata.game_version
const loader = instance.value.metadata.loader
const selectedVersion = versions.value.find(
(v) =>
v.game_versions.includes(gameVersion) &&
(data.value.project_type === 'mod'
? v.loaders.includes(loader) || v.loaders.includes('minecraft')
: true),
)
if (!selectedVersion) {
incompatibilityWarning.value.show(
instance.value,
data.value.title,
versions.value,
markInstalled,
data.value.id,
data.value.project_type,
)
installing.value = false
return
} else {
queuedVersionData = selectedVersion
await installMod(instance.value.path, selectedVersion.id).catch(handleError)
await installVersionDependencies(instance.value, queuedVersionData)
installedVersion.value = selectedVersion.id
mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
id: data.value.id,
project_type: data.value.project_type,
version_id: queuedVersionData.id,
title: data.value.title,
source: 'ProjectPage',
})
}
} else {
const gameVersion = instance.value.metadata.game_version
const loader = instance.value.metadata.loader
const compatible = versions.value.some(
(v) =>
v.game_versions.includes(gameVersion) &&
(data.value.project_type === 'mod'
? v.loaders.includes(loader) || v.loaders.includes('minecraft')
: true),
)
if (compatible) {
await installMod(instance.value.path, queuedVersionData.id).catch(handleError)
await installVersionDependencies(instance.value, queuedVersionData)
installedVersion.value = queuedVersionData.id
mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version,
id: data.value.id,
project_type: data.value.project_type,
version_id: queuedVersionData.id,
title: data.value.title,
source: 'ProjectPage',
})
} else {
incompatibilityWarning.value.show(
instance.value,
data.value.title,
[queuedVersionData],
markInstalled,
data.value.id,
data.value.project_type,
)
installing.value = false
return
}
if (instance.value && version) {
installed.value = true
installedVersion.value = version
}
installed.value = true
} else {
modInstallModal.value.show(
data.value.id,
version ? [versions.value.find((v) => v.id === queuedVersionData.id)] : versions.value,
data.value.title,
data.value.project_type,
)
}
}
installing.value = false
},
)
}
const handleRightClick = (e) => {

View File

@@ -190,6 +190,7 @@ import { ref, watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { SwapIcon } from '@/assets/icons'
import { get_project_many, get_version_many } from '@/helpers/cache.js'
const breadcrumbs = useBreadcrumbs()
@@ -204,10 +205,6 @@ const props = defineProps({
type: Array,
required: true,
},
dependencies: {
type: Object,
required: true,
},
members: {
type: Array,
required: true,
@@ -238,20 +235,45 @@ watch(
async () => {
if (route.params.version) {
version.value = props.versions.find((version) => version.id === route.params.version)
await refreshDisplayDependencies()
breadcrumbs.setName('Version', version.value.name)
}
},
)
const author = computed(() =>
props.members.find((member) => member.user.id === version.value.author_id),
props.members ? props.members.find((member) => member.user.id === version.value.author_id) : null,
)
const displayDependencies = computed(() =>
version.value.dependencies.map((dependency) => {
const version = props.dependencies.versions.find((obj) => obj.id === dependency.version_id)
const displayDependencies = ref({})
async function refreshDisplayDependencies() {
const projectIds = new Set()
const versionIds = new Set()
if (version.value.dependencies) {
for (const dependency of version.value.dependencies) {
if (dependency.project_id) {
projectIds.add(dependency.project_id)
}
if (dependency.version_id) {
versionIds.add(dependency.version_id)
}
}
}
const [projectDeps, versionDeps] = await Promise.all([
get_project_many([...projectIds]),
get_version_many([...versionIds]),
])
const dependencies = {
projects: projectDeps,
versions: versionDeps,
}
displayDependencies.value = version.value.dependencies.map((dependency) => {
const version = dependencies.versions.find((obj) => obj.id === dependency.version_id)
if (version) {
const project = props.dependencies.projects.find(
const project = dependencies.projects.find(
(obj) => obj.id === version.project_id || obj.id === dependency.project_id,
)
return {
@@ -261,7 +283,7 @@ const displayDependencies = computed(() =>
link: `/project/${project.slug}/version/${version.id}`,
}
} else {
const project = props.dependencies.projects.find((obj) => obj.id === dependency.project_id)
const project = dependencies.projects.find((obj) => obj.id === dependency.project_id)
if (project) {
return {
@@ -279,8 +301,9 @@ const displayDependencies = computed(() =>
}
}
}
}),
)
})
}
await refreshDisplayDependencies()
</script>
<style scoped lang="scss">

View File

@@ -187,8 +187,8 @@ const props = defineProps({
})
const filterVersions = ref([])
const filterLoader = ref(props.instance ? [props.instance?.metadata?.loader] : [])
const filterGameVersions = ref(props.instance ? [props.instance?.metadata?.game_version] : [])
const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
const currentPage = ref(1)

View File

@@ -0,0 +1,182 @@
import { defineStore } from 'pinia'
import {
add_project_from_version,
check_installed,
list,
get,
get_projects,
remove_project,
} from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { get_project, get_version_many } from '@/helpers/cache.js'
import { install as packInstall } from '@/helpers/pack.js'
import { mixpanel_track } from '@/helpers/mixpanel.js'
import dayjs from 'dayjs'
export const useInstall = defineStore('installStore', {
state: () => ({
installConfirmModal: null,
modInstallModal: null,
incompatibilityWarningModal: null,
}),
actions: {
setInstallConfirmModal(ref) {
this.installConfirmModal = ref
},
showInstallConfirmModal(project, version_id, onInstall) {
this.installConfirmModal.show(project, version_id, onInstall)
},
setIncompatibilityWarningModal(ref) {
this.incompatibilityWarningModal = ref
},
showIncompatibilityWarningModal(instance, project, versions, onInstall) {
this.incompatibilityWarningModal.show(instance, project, versions, onInstall)
},
setModInstallModal(ref) {
this.modInstallModal = ref
},
showModInstallModal(project, versions, onInstall) {
this.modInstallModal.show(project, versions, onInstall)
},
},
})
export const install = async (projectId, versionId, instancePath, source, callback = () => {}) => {
const project = await get_project(projectId).catch(handleError)
if (project.project_type === 'modpack') {
const version = versionId ?? project.versions[project.versions.length - 1]
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)
mixpanel_track('PackInstall', {
id: project.id,
version_id: version,
title: project.title,
source,
})
callback(version)
} else {
const install = useInstall()
install.showInstallConfirmModal(project, version, callback)
}
} else {
if (instancePath) {
const [instance, instanceProjects, versions] = await Promise.all([
await get(instancePath).catch(handleError),
await get_projects(instancePath).catch(handleError),
await get_version_many(project.versions),
])
const projectVersions = versions.sort(
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
)
let version
if (versionId) {
version = projectVersions.find((x) => x.id === versionId)
} else {
version = projectVersions.find(
(v) =>
v.game_versions.includes(instance.game_version) &&
(project.project_type === 'mod'
? v.loaders.includes(instance.loader) || v.loaders.includes('minecraft')
: true),
)
}
if (!version) {
version = projectVersions[0]
}
if (
version.game_versions.includes(instance.game_version) &&
(project.project_type === 'mod'
? version.loaders.includes(instance.loader) || version.loaders.includes('minecraft')
: true)
) {
for (const [path, file] of Object.entries(instanceProjects)) {
if (file.metadata && file.metadata.project_id === project.id) {
await remove_project(instance.path, path)
}
}
await add_project_from_version(instance.path, version.id).catch(handleError)
await installVersionDependencies(instance, version)
mixpanel_track('ProjectInstall', {
loader: instance.loader,
game_version: instance.game_version,
id: project.id,
project_type: project.project_type,
version_id: version.id,
title: project.title,
source,
})
callback(version.id)
} else {
const install = useInstall()
install.showIncompatibilityWarningModal(instance, project, projectVersions, callback)
}
} else {
const versions = (await get_version_many(project.versions).catch(handleError)).sort(
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
)
const install = useInstall()
install.showModInstallModal(project, versions, callback)
}
}
// If project is modpack:
// - We check all available instances if modpack is already installed
// If true: show confirmation modal
// If false: install it (latest version if passed version is null)
// If project is mod:
// - If instance is selected:
// - If project is already installed
// We first uninstall the project
// - If no version is selected, we look check the instance for versions to select based on the versions
// - If there are no versions, we show the incompat modal
// - If a version is selected, and the version is incompatible, we show the incompat modal
// - Version is inarlled, as well as version dependencies
}
export const installVersionDependencies = async (profile, version) => {
for (const dep of version.dependencies) {
if (dep.dependency_type !== 'required') continue
// disallow fabric api install on quilt
if (dep.project_id === 'P7dR8mSH' && profile.loader === 'quilt') continue
if (dep.version_id) {
if (
dep.project_id &&
(await check_installed(profile.path, dep.project_id).catch(handleError))
)
continue
await add_project_from_version(profile.path, dep.version_id)
} else {
if (
dep.project_id &&
(await check_installed(profile.path, dep.project_id).catch(handleError))
)
continue
const depProject = await get_project(dep.project_id).catch(handleError)
const depVersions = (await get_version_many(depProject.versions).catch(handleError)).sort(
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
)
const latest = depVersions.find(
(v) => v.game_versions.includes(profile.game_version) && v.loaders.includes(profile.loader),
)
if (latest) {
await add_project_from_version(profile.path, latest.id).catch(handleError)
}
}
}
}

View File

@@ -2,5 +2,6 @@ import { useTheming } from './theme'
import { useBreadcrumbs } from './breadcrumbs'
import { useLoading } from './loading'
import { useNotifications, handleError } from './notifications'
import { useInstall } from './install'
export { useTheming, useBreadcrumbs, useLoading, useNotifications, handleError }
export { useTheming, useBreadcrumbs, useLoading, useNotifications, handleError, useInstall }

View File

@@ -19,7 +19,6 @@ dunce = "1.0.3"
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3"
daedalus = {version = "0.1.15", features = ["bincode"] }
uuid = { version = "1.1", features = ["serde", "v4"] }
tracing = "0.1.37"

View File

@@ -3,6 +3,8 @@
windows_subsystem = "windows"
)]
use theseus::pack::install_from::{get_profile_from_pack, CreatePackLocation};
use theseus::pack::install_mrpack::install_zipped_mrpack;
use theseus::prelude::*;
use theseus::profile::create::profile_create;
@@ -39,21 +41,12 @@ async fn main() -> theseus::Result<()> {
let _log_guard = theseus::start_logger();
// Initialize state
let st = State::get().await?;
//State::update();
State::init().await?;
if minecraft_auth::users().await?.is_empty() {
println!("No users found, authenticating.");
authenticate_run().await?; // could take credentials from here direct, but also deposited in state users
}
// Autodetect java globals
st.settings.write().await.max_concurrent_downloads = 50;
st.settings.write().await.hooks.post_exit =
Some("echo This is after Minecraft runs- global setting!".to_string());
// Changed the settings, so need to reset the semaphore
st.reset_fetch_semaphore().await;
//
// st.settings
// .write()
@@ -63,56 +56,59 @@ async fn main() -> theseus::Result<()> {
// Clear profiles
println!("Clearing profiles.");
{
let h = profile::list(None).await?;
for (path, _) in h.into_iter() {
profile::remove(&path).await?;
let h = profile::list().await?;
for profile in h.into_iter() {
profile::remove(&profile.path).await?;
}
}
println!("Creating/adding profile.");
let name = "Example".to_string();
let game_version = "1.19.2".to_string();
let modloader = ModLoader::Vanilla;
let loader_version = "stable".to_string();
// let name = "Example".to_string();
// let game_version = "1.21".to_string();
// let modloader = ModLoader::Fabric;
// let loader_version = "stable".to_string();
let pack = CreatePackLocation::FromVersionId {
project_id: "1KVo5zza".to_string(),
version_id: "lKloE8SA".to_string(),
title: "Fabulously Optimized".to_string(),
icon_url: Some("https://cdn.modrinth.com/data/1KVo5zza/d8152911f8fd5d7e9a8c499fe89045af81fe816e.png".to_string()),
};
let profile = get_profile_from_pack(pack.clone());
let profile_path = profile_create(
name.clone(),
game_version,
modloader,
Some(loader_version),
None,
None,
profile.name,
profile.game_version,
profile.modloader,
profile.loader_version,
None,
None,
None,
)
.await?;
install_zipped_mrpack(pack, profile_path.to_string()).await?;
State::sync().await?;
let projects = profile::get_projects(&profile_path).await?;
for (path, file) in projects {
println!(
"{path} {} {:?} {:?}",
file.file_name, file.update_version_id, file.metadata
)
}
println!("running");
// Run a profile, running minecraft and store the RwLock to the process
let proc_lock = profile::run(&profile_path).await?;
let uuid = proc_lock.read().await.uuid;
let pid = proc_lock.read().await.current_child.read().await.id();
let process = profile::run(&profile_path).await?;
println!("Minecraft UUID: {}", uuid);
println!("Minecraft PID: {:?}", pid);
println!("Minecraft PID: {}", process.pid);
println!(
"All running process UUID {:?}",
process::get_all_running_uuids().await?
);
println!(
"All running process paths {:?}",
process::get_all_running_profile_paths().await?
);
println!("All running process UUID {:?}", process::get_all().await?);
// hold the lock to the process until it ends
println!("Waiting for process to end...");
let mut proc = proc_lock.write().await;
process::wait_for(&mut proc).await?;
process.wait_for().await?;
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus_gui"
version = "0.7.2"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
@@ -28,7 +28,7 @@ tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3"
daedalus = {version = "0.1.15", features = ["bincode"] }
daedalus = "0.2.2"
chrono = "0.4.26"
dirs = "5.0.1"
@@ -46,6 +46,9 @@ sentry-rust-minidump = "0.7.0"
lazy_static = "1"
once_cell = "1"
dashmap = "6.0.1"
paste = "1.0.15"
[target.'cfg(not(target_os = "linux"))'.dependencies]
window-shadows = "0.2.1"

View File

@@ -7,7 +7,7 @@
<dict>
<key>CFBundleURLName</key>
<!-- Obviously needs to be replaced with your app's bundle identifier -->
<string>com.modrinth.theseus</string>
<string>ModrinthApp</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- register the myapp:// and myscheme:// schemes -->
@@ -27,7 +27,7 @@
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.modrinth.theseus-type</string>
<string>ModrinthApp-type</string>
</array>
<key>NSDocumentClass</key>
<string>NSDocument</string>
@@ -45,7 +45,7 @@
<key>UTTypeIcons</key>
<dict/>
<key>UTTypeIdentifier</key>
<string>com.modrinth.theseus-type</string>
<string>ModrinthApp-type</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>

View File

@@ -11,7 +11,6 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
auth_set_default_user,
auth_remove_user,
auth_users,
auth_get_user,
])
.build()
}
@@ -96,11 +95,3 @@ pub async fn auth_set_default_user(user: uuid::Uuid) -> Result<()> {
pub async fn auth_users() -> Result<Vec<Credentials>> {
Ok(minecraft_auth::users().await?)
}
/// Get a user from the UUID
/// Prefer to use refresh instead, as it will refresh the credentials as well
// invoke('plugin:auth|auth_users',user)
#[tauri::command]
pub async fn auth_get_user(user: uuid::Uuid) -> Result<Credentials> {
Ok(minecraft_auth::get_user(user).await?)
}

56
apps/app/src/api/cache.rs Normal file
View File

@@ -0,0 +1,56 @@
use crate::api::Result;
use theseus::prelude::*;
macro_rules! impl_cache_methods {
($(($variant:ident, $type:ty)),*) => {
$(
paste::paste! {
#[tauri::command]
pub async fn [<get_ $variant:snake>](id: &str) -> Result<Option<$type>>
{
Ok(theseus::cache::[<get_ $variant:snake>](id).await?)
}
#[tauri::command]
pub async fn [<get_ $variant:snake _many>](
ids: Vec<String>,
) -> Result<Vec<$type>>
{
let ids = ids.iter().map(|x| &**x).collect::<Vec<&str>>();
let entries =
theseus::cache::[<get_ $variant:snake _many>](&*ids).await?;
Ok(entries)
}
}
)*
}
}
impl_cache_methods!(
(Project, Project),
(Version, Version),
(User, User),
(Team, Vec<TeamMember>),
(Organization, Organization),
(SearchResults, SearchResults)
);
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("cache")
.invoke_handler(tauri::generate_handler![
get_project,
get_project_many,
get_version,
get_version_many,
get_user,
get_user_many,
get_team,
get_team_many,
get_organization,
get_organization_many,
get_search_results,
get_search_results_many,
])
.build()
}

View File

@@ -4,7 +4,6 @@ use crate::api::Result;
use theseus::pack::import::ImportLauncherType;
use theseus::pack::import;
use theseus::prelude::ProfilePathId;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("import")
@@ -33,7 +32,7 @@ pub async fn import_get_importable_instances(
/// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1")
#[tauri::command]
pub async fn import_import_instance(
profile_path: ProfilePathId,
profile_path: &str,
launcher_type: ImportLauncherType,
base_path: PathBuf,
instance_folder: String,

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf;
use crate::api::Result;
use dashmap::DashMap;
use std::path::PathBuf;
use tauri::plugin::TauriPlugin;
use theseus::prelude::JavaVersion;
use theseus::prelude::*;
@@ -8,6 +8,8 @@ use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("jre")
.invoke_handler(tauri::generate_handler![
get_java_versions,
set_java_version,
jre_find_filtered_jres,
jre_get_jre,
jre_test_jre,
@@ -17,6 +19,17 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
.build()
}
#[tauri::command]
pub async fn get_java_versions() -> Result<DashMap<u32, JavaVersion>> {
Ok(jre::get_java_versions().await?)
}
#[tauri::command]
pub async fn set_java_version(java_version: JavaVersion) -> Result<()> {
jre::set_java_version(java_version).await?;
Ok(())
}
// Finds the installation of Java 8, if it exists
#[tauri::command]
pub async fn jre_find_filtered_jres(

View File

@@ -1,9 +1,6 @@
use crate::api::Result;
use theseus::logs::LogType;
use theseus::{
logs::{self, CensoredString, LatestLogCursor, Logs},
prelude::ProfilePathId,
};
use theseus::logs::{self, CensoredString, LatestLogCursor, Logs};
/*
A log is a struct containing the filename string, stdout, and stderr, as follows:
@@ -31,7 +28,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
/// Get all Logs for a profile, sorted by filename
#[tauri::command]
pub async fn logs_get_logs(
profile_path: ProfilePathId,
profile_path: &str,
clear_contents: Option<bool>,
) -> Result<Vec<Logs>> {
let val = logs::get_logs(profile_path, clear_contents).await?;
@@ -42,7 +39,7 @@ pub async fn logs_get_logs(
/// Get a Log struct for a profile by profile id and filename string
#[tauri::command]
pub async fn logs_get_logs_by_filename(
profile_path: ProfilePathId,
profile_path: &str,
log_type: LogType,
filename: String,
) -> Result<Logs> {
@@ -52,37 +49,23 @@ pub async fn logs_get_logs_by_filename(
/// Get the stdout for a profile by profile id and filename string
#[tauri::command]
pub async fn logs_get_output_by_filename(
profile_path: ProfilePathId,
profile_path: &str,
log_type: LogType,
filename: String,
) -> Result<CensoredString> {
let profile_path = if let Some(p) =
crate::profile::get(&profile_path, None).await?
{
p.profile_id()
} else {
return Err(theseus::Error::from(
theseus::ErrorKind::UnmanagedProfileError(profile_path.to_string()),
)
.into());
};
Ok(
logs::get_output_by_filename(&profile_path, log_type, &filename)
.await?,
)
Ok(logs::get_output_by_filename(profile_path, log_type, &filename).await?)
}
/// Delete all logs for a profile by profile id
#[tauri::command]
pub async fn logs_delete_logs(profile_path: ProfilePathId) -> Result<()> {
pub async fn logs_delete_logs(profile_path: &str) -> Result<()> {
Ok(logs::delete_logs(profile_path).await?)
}
/// Delete a log for a profile by profile id and filename string
#[tauri::command]
pub async fn logs_delete_logs_by_filename(
profile_path: ProfilePathId,
profile_path: &str,
log_type: LogType,
filename: String,
) -> Result<()> {
@@ -95,7 +78,7 @@ pub async fn logs_delete_logs_by_filename(
/// Get live log from a cursor
#[tauri::command]
pub async fn logs_get_latest_log_cursor(
profile_path: ProfilePathId,
profile_path: &str,
cursor: u64, // 0 to start at beginning of file
) -> Result<LatestLogCursor> {
Ok(logs::get_latest_log_cursor(profile_path, cursor).await?)

View File

@@ -6,10 +6,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("metadata")
.invoke_handler(tauri::generate_handler![
metadata_get_game_versions,
metadata_get_fabric_versions,
metadata_get_forge_versions,
metadata_get_quilt_versions,
metadata_get_neoforge_versions,
metadata_get_loader_versions,
])
.build()
}
@@ -22,24 +19,6 @@ pub async fn metadata_get_game_versions() -> Result<VersionManifest> {
/// Gets the fabric versions from daedalus
#[tauri::command]
pub async fn metadata_get_fabric_versions() -> Result<Manifest> {
Ok(theseus::metadata::get_fabric_versions().await?)
}
/// Gets the forge versions from daedalus
#[tauri::command]
pub async fn metadata_get_forge_versions() -> Result<Manifest> {
Ok(theseus::metadata::get_forge_versions().await?)
}
/// Gets the quilt versions from daedalus
#[tauri::command]
pub async fn metadata_get_quilt_versions() -> Result<Manifest> {
Ok(theseus::metadata::get_quilt_versions().await?)
}
/// Gets the quilt versions from daedalus
#[tauri::command]
pub async fn metadata_get_neoforge_versions() -> Result<Manifest> {
Ok(theseus::metadata::get_neoforge_versions().await?)
pub async fn metadata_get_loader_versions(loader: &str) -> Result<Manifest> {
Ok(theseus::metadata::get_loader_versions(loader).await?)
}

View File

@@ -16,6 +16,8 @@ pub mod settings;
pub mod tags;
pub mod utils;
pub mod cache;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
// // Main returnable Theseus GUI error

View File

@@ -1,17 +1,15 @@
use crate::api::Result;
use chrono::{Duration, Utc};
use tauri::plugin::TauriPlugin;
use tauri::{Manager, UserAttentionType};
use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("mr_auth")
.invoke_handler(tauri::generate_handler![
authenticate_begin_flow,
authenticate_await_completion,
cancel_flow,
login_pass,
login_2fa,
create_account,
refresh,
logout,
get,
])
@@ -19,19 +17,68 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
}
#[tauri::command]
pub async fn authenticate_begin_flow(provider: &str) -> Result<String> {
Ok(theseus::mr_auth::authenticate_begin_flow(provider).await?)
}
pub async fn modrinth_auth_login(
app: tauri::AppHandle,
provider: &str,
) -> Result<Option<ModrinthCredentialsResult>> {
let redirect_uri = mr_auth::authenticate_begin_flow(provider);
#[tauri::command]
pub async fn authenticate_await_completion() -> Result<ModrinthCredentialsResult>
{
Ok(theseus::mr_auth::authenticate_await_complete_flow().await?)
}
let start = Utc::now();
#[tauri::command]
pub async fn cancel_flow() -> Result<()> {
Ok(theseus::mr_auth::cancel_flow().await?)
if let Some(window) = app.get_window("modrinth-signin") {
window.close()?;
}
let window = tauri::WindowBuilder::new(
&app,
"modrinth-signin",
tauri::WindowUrl::External(redirect_uri.parse().map_err(|_| {
theseus::ErrorKind::OtherError(
"Error parsing auth redirect URL".to_string(),
)
.as_error()
})?),
)
.title("Sign into Modrinth")
.always_on_top(true)
.center()
.build()?;
window.request_user_attention(Some(UserAttentionType::Critical))?;
while (Utc::now() - start) < Duration::minutes(10) {
if window.title().is_err() {
// user closed window, cancelling flow
return Ok(None);
}
if window
.url()
.as_str()
.starts_with("https://launcher-files.modrinth.com/detect.txt")
{
let query = window
.url()
.query_pairs()
.map(|(key, val)| {
(
key.to_string(),
serde_json::Value::String(val.to_string()),
)
})
.collect();
window.close()?;
let val = mr_auth::authenticate_finish_flow(query).await?;
return Ok(Some(val));
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
window.close()?;
Ok(None)
}
#[tauri::command]
@@ -66,11 +113,6 @@ pub async fn create_account(
.await?)
}
#[tauri::command]
pub async fn refresh() -> Result<()> {
Ok(theseus::mr_auth::refresh().await?)
}
#[tauri::command]
pub async fn logout() -> Result<()> {
Ok(theseus::mr_auth::logout().await?)

View File

@@ -20,8 +20,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
#[tauri::command]
pub async fn pack_install(
location: CreatePackLocation,
profile: ProfilePathId,
) -> Result<ProfilePathId> {
profile: String,
) -> Result<String> {
Ok(install_zipped_mrpack(location, profile).await?)
}

View File

@@ -1,78 +1,33 @@
use crate::api::Result;
use theseus::prelude::*;
use uuid::Uuid;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("process")
.invoke_handler(tauri::generate_handler![
process_has_finished_by_uuid,
process_get_exit_status_by_uuid,
process_get_all_uuids,
process_get_all_running_uuids,
process_get_uuids_by_profile_path,
process_get_all_running_profile_paths,
process_get_all_running_profiles,
process_kill_by_uuid,
process_wait_for_by_uuid,
process_get_all,
process_get_by_profile_path,
process_kill,
process_wait_for,
])
.build()
}
// Checks if a process has finished by process UUID
#[tauri::command]
pub async fn process_has_finished_by_uuid(uuid: Uuid) -> Result<bool> {
Ok(process::has_finished_by_uuid(uuid).await?)
pub async fn process_get_all() -> Result<Vec<Process>> {
Ok(process::get_all().await?)
}
// Gets process exit status by process UUID
#[tauri::command]
pub async fn process_get_exit_status_by_uuid(
uuid: Uuid,
) -> Result<Option<i32>> {
Ok(process::get_exit_status_by_uuid(uuid).await?)
pub async fn process_get_by_profile_path(path: &str) -> Result<Vec<Process>> {
Ok(process::get_by_profile_path(path).await?)
}
// Gets all process UUIDs
#[tauri::command]
pub async fn process_get_all_uuids() -> Result<Vec<Uuid>> {
Ok(process::get_all_uuids().await?)
pub async fn process_kill(pid: i32) -> Result<()> {
Ok(process::kill(pid).await?)
}
// Gets all running process UUIDs
#[tauri::command]
pub async fn process_get_all_running_uuids() -> Result<Vec<Uuid>> {
Ok(process::get_all_running_uuids().await?)
}
// Gets all process UUIDs by profile path
#[tauri::command]
pub async fn process_get_uuids_by_profile_path(
profile_path: ProfilePathId,
) -> Result<Vec<Uuid>> {
Ok(process::get_uuids_by_profile_path(profile_path).await?)
}
// Gets the Profile paths of each *running* stored process in the state
#[tauri::command]
pub async fn process_get_all_running_profile_paths(
) -> Result<Vec<ProfilePathId>> {
Ok(process::get_all_running_profile_paths().await?)
}
// Gets the Profiles (cloned) of each *running* stored process in the state
#[tauri::command]
pub async fn process_get_all_running_profiles() -> Result<Vec<Profile>> {
Ok(process::get_all_running_profiles().await?)
}
// Kill a process by process UUID
#[tauri::command]
pub async fn process_kill_by_uuid(uuid: Uuid) -> Result<()> {
Ok(process::kill_by_uuid(uuid).await?)
}
// Wait for a process to finish by process UUID
#[tauri::command]
pub async fn process_wait_for_by_uuid(uuid: Uuid) -> Result<()> {
Ok(process::wait_for_by_uuid(uuid).await?)
pub async fn process_wait_for(pid: i32) -> Result<()> {
Ok(process::wait_for(pid).await?)
}

View File

@@ -1,16 +1,17 @@
use crate::api::Result;
use daedalus::modded::LoaderVersion;
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use theseus::{prelude::*, InnerProjectPathUnix};
use uuid::Uuid;
use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("profile")
.invoke_handler(tauri::generate_handler![
profile_remove,
profile_get,
profile_get_many,
profile_get_projects,
profile_get_optimal_jre_key,
profile_get_full_path,
profile_get_mod_full_path,
@@ -26,9 +27,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
profile_update_managed_modrinth_version,
profile_repair_managed_modrinth,
profile_run,
profile_run_wait,
profile_run_credentials,
profile_run_wait_credentials,
profile_kill,
profile_edit,
profile_edit_icon,
profile_export_mrpack,
@@ -40,27 +40,39 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
// Remove a profile
// invoke('plugin:profile|profile_add_path',path)
#[tauri::command]
pub async fn profile_remove(path: ProfilePathId) -> Result<()> {
profile::remove(&path).await?;
pub async fn profile_remove(path: &str) -> Result<()> {
profile::remove(path).await?;
Ok(())
}
// Get a profile by path
// invoke('plugin:profile|profile_add_path',path)
#[tauri::command]
pub async fn profile_get(
path: ProfilePathId,
clear_projects: Option<bool>,
) -> Result<Option<Profile>> {
let res = profile::get(&path, clear_projects).await?;
pub async fn profile_get(path: &str) -> Result<Option<Profile>> {
let res = profile::get(path).await?;
Ok(res)
}
#[tauri::command]
pub async fn profile_get_many(paths: Vec<String>) -> Result<Vec<Profile>> {
let ids = paths.iter().map(|x| &**x).collect::<Vec<&str>>();
let entries = profile::get_many(&ids).await?;
Ok(entries)
}
#[tauri::command]
pub async fn profile_get_projects(
path: &str,
) -> Result<DashMap<String, ProfileFile>> {
let res = profile::get_projects(path).await?;
Ok(res)
}
// Get a profile's full path
// invoke('plugin:profile|profile_get_full_path',path)
#[tauri::command]
pub async fn profile_get_full_path(path: ProfilePathId) -> Result<PathBuf> {
let res = profile::get_full_path(&path).await?;
pub async fn profile_get_full_path(path: &str) -> Result<PathBuf> {
let res = profile::get_full_path(path).await?;
Ok(res)
}
@@ -68,43 +80,41 @@ pub async fn profile_get_full_path(path: ProfilePathId) -> Result<PathBuf> {
// invoke('plugin:profile|profile_get_mod_full_path',path)
#[tauri::command]
pub async fn profile_get_mod_full_path(
path: ProfilePathId,
project_path: ProjectPathId,
path: &str,
project_path: &str,
) -> Result<PathBuf> {
let res = profile::get_mod_full_path(&path, &project_path).await?;
let res = profile::get_mod_full_path(path, project_path).await?;
Ok(res)
}
// Get optimal java version from profile
#[tauri::command]
pub async fn profile_get_optimal_jre_key(
path: ProfilePathId,
path: &str,
) -> Result<Option<JavaVersion>> {
let res = profile::get_optimal_jre_key(&path).await?;
let res = profile::get_optimal_jre_key(path).await?;
Ok(res)
}
// Get a copy of the profile set
// invoke('plugin:profile|profile_list')
#[tauri::command]
pub async fn profile_list(
clear_projects: Option<bool>,
) -> Result<HashMap<ProfilePathId, Profile>> {
let res = profile::list(clear_projects).await?;
pub async fn profile_list() -> Result<Vec<Profile>> {
let res = profile::list().await?;
Ok(res)
}
#[tauri::command]
pub async fn profile_check_installed(
path: ProfilePathId,
project_id: String,
path: &str,
project_id: &str,
) -> Result<bool> {
let profile = profile_get(path, None).await?;
if let Some(profile) = profile {
Ok(profile.projects.into_iter().any(|(_, project)| {
if let ProjectMetadata::Modrinth { project, .. } = &project.metadata
{
project.id == project_id
let check_project_id = project_id;
if let Ok(projects) = profile::get_projects(path).await {
Ok(projects.into_iter().any(|(_, project)| {
if let Some(metadata) = &project.metadata {
check_project_id == metadata.project_id
} else {
false
}
@@ -117,49 +127,47 @@ pub async fn profile_check_installed(
/// Installs/Repairs a profile
/// invoke('plugin:profile|profile_install')
#[tauri::command]
pub async fn profile_install(path: ProfilePathId, force: bool) -> Result<()> {
profile::install(&path, force).await?;
pub async fn profile_install(path: &str, force: bool) -> Result<()> {
profile::install(path, force).await?;
Ok(())
}
/// Updates all of the profile's projects
/// invoke('plugin:profile|profile_update_all')
#[tauri::command]
pub async fn profile_update_all(
path: ProfilePathId,
) -> Result<HashMap<ProjectPathId, ProjectPathId>> {
Ok(profile::update_all_projects(&path).await?)
pub async fn profile_update_all(path: &str) -> Result<HashMap<String, String>> {
Ok(profile::update_all_projects(path).await?)
}
/// Updates a specified project
/// invoke('plugin:profile|profile_update_project')
#[tauri::command]
pub async fn profile_update_project(
path: ProfilePathId,
project_path: ProjectPathId,
) -> Result<ProjectPathId> {
Ok(profile::update_project(&path, &project_path, None).await?)
path: &str,
project_path: &str,
) -> Result<String> {
Ok(profile::update_project(path, project_path, None).await?)
}
// Adds a project to a profile from a version ID
// invoke('plugin:profile|profile_add_project_from_version')
#[tauri::command]
pub async fn profile_add_project_from_version(
path: ProfilePathId,
version_id: String,
) -> Result<ProjectPathId> {
Ok(profile::add_project_from_version(&path, version_id).await?)
path: &str,
version_id: &str,
) -> Result<String> {
Ok(profile::add_project_from_version(path, version_id).await?)
}
// Adds a project to a profile from a path
// invoke('plugin:profile|profile_add_project_from_path')
#[tauri::command]
pub async fn profile_add_project_from_path(
path: ProfilePathId,
path: &str,
project_path: &Path,
project_type: Option<String>,
) -> Result<ProjectPathId> {
let res = profile::add_project_from_path(&path, project_path, project_type)
project_type: Option<ProjectType>,
) -> Result<String> {
let res = profile::add_project_from_path(path, project_path, project_type)
.await?;
Ok(res)
}
@@ -168,27 +176,27 @@ pub async fn profile_add_project_from_path(
// invoke('plugin:profile|profile_toggle_disable_project')
#[tauri::command]
pub async fn profile_toggle_disable_project(
path: ProfilePathId,
project_path: ProjectPathId,
) -> Result<ProjectPathId> {
Ok(profile::toggle_disable_project(&path, &project_path).await?)
path: &str,
project_path: &str,
) -> Result<String> {
Ok(profile::toggle_disable_project(path, project_path).await?)
}
// Removes a project from a profile
// invoke('plugin:profile|profile_remove_project')
#[tauri::command]
pub async fn profile_remove_project(
path: ProfilePathId,
project_path: ProjectPathId,
path: &str,
project_path: &str,
) -> Result<()> {
profile::remove_project(&path, &project_path).await?;
profile::remove_project(path, project_path).await?;
Ok(())
}
// Updates a managed Modrinth profile to a version of version_id
#[tauri::command]
pub async fn profile_update_managed_modrinth_version(
path: ProfilePathId,
path: String,
version_id: String,
) -> Result<()> {
Ok(
@@ -199,17 +207,15 @@ pub async fn profile_update_managed_modrinth_version(
// Repairs a managed Modrinth profile by updating it to the current version
#[tauri::command]
pub async fn profile_repair_managed_modrinth(
path: ProfilePathId,
) -> Result<()> {
Ok(profile::update::repair_managed_modrinth(&path).await?)
pub async fn profile_repair_managed_modrinth(path: &str) -> Result<()> {
Ok(profile::update::repair_managed_modrinth(path).await?)
}
// Exports a profile to a .mrpack file (export_location should end in .mrpack)
// invoke('profile_export_mrpack')
#[tauri::command]
pub async fn profile_export_mrpack(
path: ProfilePathId,
path: &str,
export_location: PathBuf,
included_overrides: Vec<String>,
version_id: Option<String>,
@@ -217,7 +223,7 @@ pub async fn profile_export_mrpack(
name: Option<String>, // only used to cache
) -> Result<()> {
profile::export_mrpack(
&path,
path,
export_location,
included_overrides,
version_id,
@@ -231,9 +237,9 @@ pub async fn profile_export_mrpack(
/// See [`profile::get_pack_export_candidates`]
#[tauri::command]
pub async fn profile_get_pack_export_candidates(
profile_path: ProfilePathId,
) -> Result<Vec<InnerProjectPathUnix>> {
let candidates = profile::get_pack_export_candidates(&profile_path).await?;
profile_path: &str,
) -> Result<Vec<String>> {
let candidates = profile::get_pack_export_candidates(profile_path).await?;
Ok(candidates)
}
@@ -242,19 +248,10 @@ pub async fn profile_get_pack_export_candidates(
// for the actual Child in the state.
// invoke('plugin:profile|profile_run', path)
#[tauri::command]
pub async fn profile_run(path: ProfilePathId) -> Result<Uuid> {
let minecraft_child = profile::run(&path).await?;
let uuid = minecraft_child.read().await.uuid;
Ok(uuid)
}
pub async fn profile_run(path: &str) -> Result<Process> {
let process = profile::run(path).await?;
// Run Minecraft using a profile using the default credentials, and wait for the result
// invoke('plugin:profile|profile_run_wait', path)
#[tauri::command]
pub async fn profile_run_wait(path: ProfilePathId) -> Result<()> {
let proc_lock = profile::run(&path).await?;
let mut proc = proc_lock.write().await;
Ok(process::wait_for(&mut proc).await?)
Ok(process)
}
// Run Minecraft using a profile using chosen credentials
@@ -263,85 +260,83 @@ pub async fn profile_run_wait(path: ProfilePathId) -> Result<()> {
// invoke('plugin:profile|profile_run_credentials', {path, credentials})')
#[tauri::command]
pub async fn profile_run_credentials(
path: ProfilePathId,
path: &str,
credentials: Credentials,
) -> Result<Uuid> {
let minecraft_child = profile::run_credentials(&path, &credentials).await?;
let uuid = minecraft_child.read().await.uuid;
) -> Result<Process> {
let process = profile::run_credentials(path, &credentials).await?;
Ok(uuid)
Ok(process)
}
// Run Minecraft using a profile using the chosen credentials, and wait for the result
// invoke('plugin:profile|profile_run_wait', {path, credentials)
#[tauri::command]
pub async fn profile_run_wait_credentials(
path: ProfilePathId,
credentials: Credentials,
) -> Result<()> {
let proc_lock = profile::run_credentials(&path, &credentials).await?;
let mut proc = proc_lock.write().await;
Ok(process::wait_for(&mut proc).await?)
pub async fn profile_kill(path: &str) -> Result<()> {
profile::kill(path).await?;
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EditProfile {
pub metadata: Option<EditProfileMetadata>,
pub java: Option<JavaSettings>,
pub memory: Option<MemorySettings>,
pub resolution: Option<WindowSize>,
pub hooks: Option<Hooks>,
pub fullscreen: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditProfileMetadata {
pub name: Option<String>,
pub game_version: Option<String>,
pub loader: Option<ModLoader>,
pub loader_version: Option<LoaderVersion>,
pub linked_data: Option<LinkedData>,
pub loader_version: Option<String>,
pub groups: Option<Vec<String>>,
pub linked_data: Option<LinkedData>,
pub java_path: Option<String>,
pub extra_launch_args: Option<Vec<String>>,
pub custom_env_vars: Option<Vec<(String, String)>>,
pub memory: Option<MemorySettings>,
pub force_fullscreen: Option<bool>,
pub game_resolution: Option<WindowSize>,
pub hooks: Option<Hooks>,
}
// Edits a profile
// invoke('plugin:profile|profile_edit', {path, editProfile})
#[tauri::command]
pub async fn profile_edit(
path: ProfilePathId,
edit_profile: EditProfile,
) -> Result<()> {
profile::edit(&path, |prof| {
if let Some(metadata) = edit_profile.metadata.clone() {
if let Some(name) = metadata.name {
prof.metadata.name = name;
}
if let Some(game_version) = metadata.game_version {
prof.metadata.game_version = game_version;
}
if let Some(loader) = metadata.loader {
prof.metadata.loader = loader;
}
prof.metadata.loader_version = metadata.loader_version;
prof.metadata.linked_data = metadata.linked_data;
pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
profile::edit(path, |prof| {
if let Some(name) = edit_profile.name.clone() {
prof.name = name;
}
if let Some(game_version) = edit_profile.game_version.clone() {
prof.game_version = game_version;
}
if let Some(loader) = edit_profile.loader {
prof.loader = loader;
}
prof.loader_version.clone_from(&edit_profile.loader_version);
prof.linked_data.clone_from(&edit_profile.linked_data);
if let Some(groups) = metadata.groups {
prof.metadata.groups = groups;
}
if let Some(groups) = edit_profile.groups.clone() {
prof.groups = groups;
}
prof.java.clone_from(&edit_profile.java);
prof.java_path.clone_from(&edit_profile.java_path);
prof.memory = edit_profile.memory;
prof.resolution = edit_profile.resolution;
prof.fullscreen = edit_profile.fullscreen;
prof.hooks.clone_from(&edit_profile.hooks);
prof.game_resolution = edit_profile.game_resolution;
prof.force_fullscreen = edit_profile.force_fullscreen;
prof.metadata.date_modified = chrono::Utc::now();
if let Some(hooks) = edit_profile.hooks.clone() {
prof.hooks = hooks;
}
prof.modified = chrono::Utc::now();
prof.custom_env_vars
.clone_from(&edit_profile.custom_env_vars);
prof.extra_launch_args
.clone_from(&edit_profile.extra_launch_args);
async { Ok(()) }
})
.await?;
State::sync().await?;
Ok(())
}
@@ -350,9 +345,9 @@ pub async fn profile_edit(
// invoke('plugin:profile|profile_edit_icon')
#[tauri::command]
pub async fn profile_edit_icon(
path: ProfilePathId,
path: &str,
icon_path: Option<&Path>,
) -> Result<()> {
profile::edit_icon(&path, icon_path).await?;
profile::edit_icon(path, icon_path).await?;
Ok(())
}

View File

@@ -1,5 +1,4 @@
use crate::api::Result;
use std::path::PathBuf;
use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
@@ -19,9 +18,9 @@ pub async fn profile_create(
game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
icon: Option<PathBuf>, // the icon for the profile
no_watch: Option<bool>,
) -> Result<ProfilePathId> {
icon: Option<String>, // the icon for the profile
skip_install: Option<bool>,
) -> Result<String> {
let res = profile::create::profile_create(
name,
game_version,
@@ -29,9 +28,7 @@ pub async fn profile_create(
loader_version,
icon,
None,
None,
None,
no_watch,
skip_install,
)
.await?;
Ok(res)
@@ -40,7 +37,7 @@ pub async fn profile_create(
// Creates a profile from a duplicate
// invoke('plugin:profile_create|profile_duplicate',profile)
#[tauri::command]
pub async fn profile_duplicate(path: ProfilePathId) -> Result<ProfilePathId> {
pub async fn profile_duplicate(path: &str) -> Result<String> {
let res = profile::create::profile_create_from_duplicate(path).await?;
Ok(res)
}

View File

@@ -1,16 +1,9 @@
use std::path::PathBuf;
use crate::api::Result;
use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("settings")
.invoke_handler(tauri::generate_handler![
settings_get,
settings_set,
settings_change_config_dir,
settings_is_dir_writeable
])
.invoke_handler(tauri::generate_handler![settings_get, settings_set])
.build()
}
@@ -29,20 +22,3 @@ pub async fn settings_set(settings: Settings) -> Result<()> {
settings::set(settings).await?;
Ok(())
}
// Change config directory
// Seizes the entire State to do it
// invoke('plugin:settings|settings_change_config_dir', new_dir)
#[tauri::command]
pub async fn settings_change_config_dir(new_config_dir: PathBuf) -> Result<()> {
settings::set_config_dir(new_config_dir).await?;
Ok(())
}
#[tauri::command]
pub async fn settings_is_dir_writeable(
new_config_dir: PathBuf,
) -> Result<bool> {
let res = settings::is_dir_writeable(new_config_dir).await?;
Ok(res)
}

View File

@@ -1,5 +1,5 @@
use crate::api::Result;
use theseus::tags::{Category, DonationPlatform, GameVersion, Loader, Tags};
use theseus::tags::{Category, DonationPlatform, GameVersion, Loader};
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("tags")
@@ -9,7 +9,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tags_get_loaders,
tags_get_game_versions,
tags_get_donation_platforms,
tags_get_tag_bundle,
])
.build()
}
@@ -43,9 +42,3 @@ pub async fn tags_get_game_versions() -> Result<Vec<GameVersion>> {
pub async fn tags_get_donation_platforms() -> Result<Vec<DonationPlatform>> {
Ok(theseus::tags::get_donation_platform_tags().await?)
}
/// Gets cached tag bundle from the database
#[tauri::command]
pub async fn tags_get_tag_bundle() -> Result<Tags> {
Ok(theseus::tags::get_tag_bundle().await?)
}

View File

@@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
use theseus::{
handler,
prelude::{CommandPayload, DirectoryInfo},
State,
};
use crate::api::Result;
@@ -16,11 +15,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
show_in_folder,
show_launcher_logs_folder,
progress_bars_list,
safety_check_safe_loading_bars,
get_opening_command,
await_sync,
is_offline,
refresh_offline
get_opening_command
])
.build()
}
@@ -54,12 +49,6 @@ pub async fn progress_bars_list(
Ok(res)
}
// Check if there are any safe loading bars running
#[tauri::command]
pub async fn safety_check_safe_loading_bars() -> Result<bool> {
Ok(theseus::safety::check_safe_loading_bars().await?)
}
// cfg only on mac os
// disables mouseover and fixes a random crash error only fixed by recent versions of macos
#[cfg(target_os = "macos")]
@@ -82,7 +71,7 @@ pub async fn should_disable_mouseover() -> bool {
}
#[tauri::command]
pub fn show_in_folder(mut path: PathBuf) -> Result<()> {
pub fn show_in_folder(path: PathBuf) -> Result<()> {
{
#[cfg(target_os = "windows")]
{
@@ -101,6 +90,7 @@ pub fn show_in_folder(mut path: PathBuf) -> Result<()> {
{
use std::fs::metadata;
let mut path = path;
let path_string = path.to_string_lossy().to_string();
if metadata(&path)?.is_dir() {
@@ -171,28 +161,3 @@ pub async fn get_opening_command() -> Result<Option<CommandPayload>> {
pub async fn handle_command(command: String) -> Result<()> {
Ok(theseus::handler::parse_and_emit_command(&command).await?)
}
// Waits for state to be synced
#[tauri::command]
pub async fn await_sync() -> Result<()> {
State::sync().await?;
tracing::debug!("State synced");
Ok(())
}
/// Check if theseus is currently in offline mode, without a refresh attempt
#[tauri::command]
pub async fn is_offline() -> Result<bool> {
let state = State::get().await?;
let offline = *state.offline.read().await;
Ok(offline)
}
/// Refreshes whether or not theseus is in offline mode, and returns the new value
#[tauri::command]
pub async fn refresh_offline() -> Result<bool> {
let state = State::get().await?;
state.refresh_offline().await?;
let offline = *state.offline.read().await;
Ok(offline)
}

View File

@@ -17,10 +17,8 @@ mod macos;
#[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app).await?;
let s = State::get().await?;
State::update();
State::init().await?;
s.children.write().await.rescue_cache().await?;
Ok(())
}
@@ -50,7 +48,7 @@ struct Payload {
// if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks
fn main() {
tauri_plugin_deep_link::prepare("com.modrinth.theseus");
tauri_plugin_deep_link::prepare("ModrinthApp");
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
@@ -128,6 +126,7 @@ fn main() {
}
})
}
let builder = builder
.plugin(api::auth::init())
.plugin(api::mr_auth::init())
@@ -142,11 +141,13 @@ fn main() {
.plugin(api::settings::init())
.plugin(api::tags::init())
.plugin(api::utils::init())
.plugin(api::cache::init())
.invoke_handler(tauri::generate_handler![
initialize_state,
is_dev,
toggle_decorations,
api::auth::auth_login,
api::mr_auth::modrinth_auth_login,
]);
builder

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "Modrinth App",
"version": "0.7.2"
"version": "0.8.0-1"
},
"tauri": {
"allowlist": {
@@ -20,8 +20,8 @@
"asset": true,
"assetScope": [
"$APPDATA/caches/icons/*",
"$APPCONFIG/caches/icons/*",
"$CONFIG/caches/icons/*"
"$APPDATA/caches/icons/*",
"$APPDATA/caches/icons/*"
]
},
"shell": {
@@ -56,7 +56,7 @@
},
"externalBin": [],
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"identifier": "com.modrinth.theseus",
"identifier": "ModrinthApp",
"longDescription": "",
"macOS": {
"entitlements": "App.entitlements",

View File

@@ -0,0 +1,158 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, default_page, collapsed_navigation, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry,\n onboarded,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated\n FROM settings\n ",
"describe": {
"columns": [
{
"name": "max_concurrent_writes",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "max_concurrent_downloads",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "theme",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "default_page",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "collapsed_navigation",
"ordinal": 4,
"type_info": "Int64"
},
{
"name": "advanced_rendering",
"ordinal": 5,
"type_info": "Int64"
},
{
"name": "native_decorations",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "discord_rpc",
"ordinal": 7,
"type_info": "Int64"
},
{
"name": "developer_mode",
"ordinal": 8,
"type_info": "Int64"
},
{
"name": "telemetry",
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "onboarded",
"ordinal": 10,
"type_info": "Int64"
},
{
"name": "extra_launch_args",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "custom_env_vars",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "mc_memory_max",
"ordinal": 13,
"type_info": "Int64"
},
{
"name": "mc_force_fullscreen",
"ordinal": 14,
"type_info": "Int64"
},
{
"name": "mc_game_resolution_x",
"ordinal": 15,
"type_info": "Int64"
},
{
"name": "mc_game_resolution_y",
"ordinal": 16,
"type_info": "Int64"
},
{
"name": "hide_on_process_start",
"ordinal": 17,
"type_info": "Int64"
},
{
"name": "hook_pre_launch",
"ordinal": 18,
"type_info": "Text"
},
{
"name": "hook_wrapper",
"ordinal": 19,
"type_info": "Text"
},
{
"name": "hook_post_exit",
"ordinal": 20,
"type_info": "Text"
},
{
"name": "custom_dir",
"ordinal": 21,
"type_info": "Text"
},
{
"name": "prev_custom_dir",
"ordinal": 22,
"type_info": "Text"
},
{
"name": "migrated",
"ordinal": 23,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
null,
null,
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
false
]
},
"hash": "03d1aeddf7788320530c447a82342aecdb4099ce183dd9106c4bcc47604cb080"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO minecraft_device_tokens (id, uuid, private_key, x, y, issue_instant, not_after, token, display_claims)\n VALUES (0, $1, $2, $3, $4, $5, $6, $7, $8)\n ON CONFLICT (id) DO UPDATE SET\n uuid = $1,\n private_key = $2,\n x = $3,\n y = $4,\n issue_instant = $5,\n not_after = $6,\n token = $7,\n display_claims = jsonb($8)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "0cfb12e0553411b01b721d1c38ef27acd240bb2ff3e07dee962bf67e20f81f36"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE minecraft_users\n SET active = FALSE\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "12f8b2b9f0acca2ea29aa6a77266b2b27efc6a0433bab1d4bbe10c69fd417494"
}

View File

@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n full_version, architecture, path\n FROM java_versions\n WHERE major_version = $1\n ",
"describe": {
"columns": [
{
"name": "full_version",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "architecture",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "path",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "1397c1825096fb402cdd3b5dae8cd3910b1719f433a0c34d40415dd7681ab272"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM profiles\n WHERE path = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "169ce6afb8e9739dacff3f4bea024ed28df292a063d615514c67a38301d71806"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM processes WHERE pid = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "1769b7033985bfdd04ee8912d9f28e0d15a8b893db47aca3aec054c7134f1f3f"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, active, session_id, expires\n FROM modrinth_users\n WHERE active = TRUE\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "active",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "session_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "expires",
"ordinal": 3,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "18881c0c2ec1b0cc73fa13b4c242dfc577061b92479ce96ffb30a457939b5ffe"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n major_version, full_version, architecture, path\n FROM java_versions\n ",
"describe": {
"columns": [
{
"name": "major_version",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "full_version",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "architecture",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "path",
"ordinal": 3,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "265f9c9ad992da0aeaf69c3f0077b54a186b98796ec549c9d891089ea33cf3fc"
}

View File

@@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, data_type, json(data) as \"data?: serde_json::Value\", alias, expires\n FROM cache\n WHERE data_type = $1 AND (\n id IN (SELECT value FROM json_each($2))\n OR\n alias IN (SELECT value FROM json_each($3))\n )\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "data_type",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "data?: serde_json::Value",
"ordinal": 2,
"type_info": "Null"
},
{
"name": "alias",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "expires",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
null,
true,
false
]
},
"hash": "28b3e3132d75e551c1fa14b8d3be36adca581f8ad1b90f85d3ec3d92ec61e65e"
}

View File

@@ -0,0 +1,50 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n pid, start_time, name, executable, profile_path, post_exit_command\n FROM processes\n WHERE 1=$1",
"describe": {
"columns": [
{
"name": "pid",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "start_time",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "executable",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "profile_path",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "post_exit_command",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "3cac786ad15ef1167bc50ca846d98facb3dee35c9e421209c1161ee7380b7a74"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE modrinth_users\n SET active = FALSE\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "45c692b305b36540139b5956dcff5bd5aeacec7d0a8abd640a7365902e57a2fd"
}

View File

@@ -0,0 +1,170 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n path, install_stage, name, icon_path,\n game_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": [
{
"name": "path",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "install_stage",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "icon_path",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "game_version",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "mod_loader",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "mod_loader_version",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"ordinal": 7,
"type_info": "Null"
},
{
"name": "linked_project_id",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "linked_version_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "locked",
"ordinal": 10,
"type_info": "Int64"
},
{
"name": "created",
"ordinal": 11,
"type_info": "Int64"
},
{
"name": "modified",
"ordinal": 12,
"type_info": "Int64"
},
{
"name": "last_played",
"ordinal": 13,
"type_info": "Int64"
},
{
"name": "submitted_time_played",
"ordinal": 14,
"type_info": "Int64"
},
{
"name": "recent_time_played",
"ordinal": 15,
"type_info": "Int64"
},
{
"name": "override_java_path",
"ordinal": 16,
"type_info": "Text"
},
{
"name": "override_extra_launch_args!: serde_json::Value",
"ordinal": 17,
"type_info": "Null"
},
{
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 18,
"type_info": "Null"
},
{
"name": "override_mc_memory_max",
"ordinal": 19,
"type_info": "Int64"
},
{
"name": "override_mc_force_fullscreen",
"ordinal": 20,
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_x",
"ordinal": 21,
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_y",
"ordinal": 22,
"type_info": "Int64"
},
{
"name": "override_hook_pre_launch",
"ordinal": 23,
"type_info": "Text"
},
{
"name": "override_hook_wrapper",
"ordinal": 24,
"type_info": "Text"
},
{
"name": "override_hook_post_exit",
"ordinal": 25,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
false,
false,
true,
null,
true,
true,
true,
false,
false,
true,
false,
false,
true,
null,
null,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "4acd47f6bad3d2d4df5e5d43b3441fa2714cb8ad978adc108acc67f042380df1"
}

View File

@@ -0,0 +1,170 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n path, install_stage, name, icon_path,\n game_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": [
{
"name": "path",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "install_stage",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "icon_path",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "game_version",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "mod_loader",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "mod_loader_version",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "groups!: serde_json::Value",
"ordinal": 7,
"type_info": "Null"
},
{
"name": "linked_project_id",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "linked_version_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "locked",
"ordinal": 10,
"type_info": "Int64"
},
{
"name": "created",
"ordinal": 11,
"type_info": "Int64"
},
{
"name": "modified",
"ordinal": 12,
"type_info": "Int64"
},
{
"name": "last_played",
"ordinal": 13,
"type_info": "Int64"
},
{
"name": "submitted_time_played",
"ordinal": 14,
"type_info": "Int64"
},
{
"name": "recent_time_played",
"ordinal": 15,
"type_info": "Int64"
},
{
"name": "override_java_path",
"ordinal": 16,
"type_info": "Text"
},
{
"name": "override_extra_launch_args!: serde_json::Value",
"ordinal": 17,
"type_info": "Null"
},
{
"name": "override_custom_env_vars!: serde_json::Value",
"ordinal": 18,
"type_info": "Null"
},
{
"name": "override_mc_memory_max",
"ordinal": 19,
"type_info": "Int64"
},
{
"name": "override_mc_force_fullscreen",
"ordinal": 20,
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_x",
"ordinal": 21,
"type_info": "Int64"
},
{
"name": "override_mc_game_resolution_y",
"ordinal": 22,
"type_info": "Int64"
},
{
"name": "override_hook_pre_launch",
"ordinal": 23,
"type_info": "Text"
},
{
"name": "override_hook_wrapper",
"ordinal": 24,
"type_info": "Text"
},
{
"name": "override_hook_post_exit",
"ordinal": 25,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
false,
false,
true,
null,
true,
true,
true,
false,
false,
true,
false,
false,
true,
null,
null,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "5265d5ad85da898855d628f6b45e39026908fc950aad3c7797be37b5d0b74094"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM modrinth_users WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "554805c9902e5a1cc4c0f03b4a633e6dc5b1d46f9c2454075eefe8df9a38f582"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO java_versions (major_version, full_version, architecture, path)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (major_version) DO UPDATE SET\n full_version = $2,\n architecture = $3,\n path = $4\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "55ad9c6b0b3172f0528e7ccd60f7c51c77946643b8f912fe265207da275a280f"
}

View File

@@ -0,0 +1,50 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n pid, start_time, name, executable, profile_path, post_exit_command\n FROM processes\n WHERE profile_path = $1",
"describe": {
"columns": [
{
"name": "pid",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "start_time",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "executable",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "profile_path",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "post_exit_command",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "5f07a8b45063167074db8b3da51e220a7a0f5879fb8978d4033e259102ae3790"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, active, session_id, expires\n FROM modrinth_users\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "active",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "session_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "expires",
"ordinal": 3,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "6d7ebc0f233dc730fa8c99c750421065f5e35f321954a9d5ae9cde907d5ce823"
}

View File

@@ -0,0 +1,62 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n uuid, private_key, x, y, issue_instant, not_after, token, json(display_claims) as \"display_claims!: serde_json::Value\"\n FROM minecraft_device_tokens\n ",
"describe": {
"columns": [
{
"name": "uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "private_key",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "x",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "y",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "issue_instant",
"ordinal": 4,
"type_info": "Int64"
},
{
"name": "not_after",
"ordinal": 5,
"type_info": "Int64"
},
{
"name": "token",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "display_claims!: serde_json::Value",
"ordinal": 7,
"type_info": "Null"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
null
]
},
"hash": "6e3fa492c085ebb8e7280dd4d55cdcf73da199ea6ac05ee3ee798ece80d877cf"
}

View File

@@ -0,0 +1,50 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires\n FROM minecraft_users\n ",
"describe": {
"columns": [
{
"name": "uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "active",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "username",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "access_token",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "refresh_token",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "expires",
"ordinal": 5,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "727e3e1bc8625bbcb833920059bb8cea926ac6c65d613904eff1d740df30acda"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO modrinth_users (id, active, session_id, expires)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (id) DO UPDATE SET\n active = $2,\n session_id = $3,\n expires = $4\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "81a80df2f3fdbbb78d45e7420609c3ae945bc499b4229906c487533d1dcb280c"
}

View File

@@ -0,0 +1,50 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires\n FROM minecraft_users\n WHERE active = TRUE\n ",
"describe": {
"columns": [
{
"name": "uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "active",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "username",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "access_token",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "refresh_token",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "expires",
"ordinal": 5,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "bf7d47350092d87c478009adaab131168e87bb37aa65c2156ad2cb6198426d8c"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO processes (pid, start_time, name, executable, profile_path, post_exit_command)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (pid) DO UPDATE SET\n start_time = $2,\n name = $3,\n executable = $4,\n profile_path = $5,\n post_exit_command = $6\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "d1b8f27c8150f9ae514a7c9ddc68f4a59f08b7df1c65758539220d7211ade682"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM minecraft_users WHERE uuid = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "d21e8a5116c43a3b511321a2655d8217f8c46b816a2f4e60c11dfcd173120e7e"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO cache (id, data_type, alias, data, expires)\n SELECT\n json_extract(value, '$.id') AS id,\n json_extract(value, '$.data_type') AS data_type,\n json_extract(value, '$.alias') AS alias,\n json_extract(value, '$.data') AS data,\n json_extract(value, '$.expires') AS expires\n FROM\n json_each($1)\n WHERE TRUE\n ON CONFLICT (id, data_type) DO UPDATE SET\n alias = excluded.alias,\n data = excluded.data,\n expires = excluded.expires\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "d63935a6e411b5ea145dfa1d4772899303d9b82b1ecd2e30dc71b411ee538f54"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n default_page = $4,\n collapsed_navigation = $5,\n advanced_rendering = $6,\n native_decorations = $7,\n\n discord_rpc = $8,\n developer_mode = $9,\n telemetry = $10,\n\n onboarded = $11,\n\n extra_launch_args = jsonb($12),\n custom_env_vars = jsonb($13),\n mc_memory_max = $14,\n mc_force_fullscreen = $15,\n mc_game_resolution_x = $16,\n mc_game_resolution_y = $17,\n hide_on_process_start = $18,\n\n hook_pre_launch = $19,\n hook_wrapper = $20,\n hook_post_exit = $21,\n\n custom_dir = $22,\n prev_custom_dir = $23,\n migrated = $24\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 24
},
"nullable": []
},
"hash": "d645daf951ff6fead3c86df685d99bacc81cb0a999c0f8d2ff7755b0089a79d8"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (uuid) DO UPDATE SET\n active = $2,\n username = $3,\n access_token = $4,\n refresh_token = $5,\n expires = $6\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "d719cf2f6f87c5ea7ea6ace2d6a1828ee58a724f06a91633b8a40b4e04d0b9a0"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n )\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 )\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 ",
"describe": {
"columns": [],
"parameters": {
"Right": 26
},
"nullable": []
},
"hash": "db1f94b9c17c790c029a7691620d6bbdcbdfcce4b069b8ed46dc3abd2f5f4e58"
}

View File

@@ -0,0 +1,50 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n pid, start_time, name, executable, profile_path, post_exit_command\n FROM processes\n WHERE pid = $1",
"describe": {
"columns": [
{
"name": "pid",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "start_time",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "executable",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "profile_path",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "post_exit_command",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "e18e960d33a140e522ca20b91d63560b921b922701b69d868dc231f6b0f4cf1c"
}

View File

@@ -1,14 +1,12 @@
[package]
name = "theseus"
version = "0.7.2"
version = "0.0.0"
authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
theseus_macros = { path = "../app-macros" }
bytes = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@@ -23,10 +21,10 @@ async_zip = { version = "0.0.17", features = ["full"] }
flate2 = "1.0.28"
tempfile = "3.5.0"
urlencoding = "2.1.3"
dashmap = { version = "6.0.1", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] }
daedalus = { version = "0.1.25" }
daedalus = { version = "0.2.2" }
dirs = "5.0.1"
regex = "1.5"
@@ -59,13 +57,18 @@ dunce = "1.0.3"
whoami = "1.4.0"
discord-rich-presence = "0.2.3"
discord-rich-presence = "0.2.4"
p256 = { version = "0.13.2", features = ["ecdsa"] }
rand = "0.8"
byteorder = "1.5.0"
base64 = "0.22.0"
# TODO: Remove when new SQLX version is released
# We force-upgrade SQLite so JSONB support is added (theseus)
# https://github.com/launchbadge/sqlx/commit/352b02de6af70f1ff1bfbd15329120589a0f7337
sqlx = { git = "https://github.com/launchbadge/sqlx.git", rev = "352b02de6af70f1ff1bfbd15329120589a0f7337", features = [ "runtime-tokio", "sqlite", "macros"] }
[target.'cfg(windows)'.dependencies]
winreg = "0.52.0"

View File

@@ -0,0 +1,158 @@
CREATE TABLE settings (
id INTEGER NOT NULL CHECK (id = 0),
max_concurrent_downloads INTEGER NOT NULL DEFAULT 10,
max_concurrent_writes INTEGER NOT NULL DEFAULT 10,
theme TEXT NOT NULL DEFAULT 'dark',
default_page TEXT NOT NULL DEFAULT 'home',
collapsed_navigation INTEGER NOT NULL DEFAULT TRUE,
advanced_rendering INTEGER NOT NULL DEFAULT TRUE,
native_decorations INTEGER NOT NULL DEFAULT FALSE,
telemetry INTEGER NOT NULL DEFAULT TRUE,
discord_rpc INTEGER NOT NULL DEFAULT TRUE,
developer_mode INTEGER NOT NULL DEFAULT FALSE,
onboarded INTEGER NOT NULL DEFAULT FALSE,
-- array of strings
extra_launch_args JSONB NOT NULL,
-- array of (string, string)
custom_env_vars JSONB NOT NULL,
mc_memory_max INTEGER NOT NULL DEFAULT 2048,
mc_force_fullscreen INTEGER NOT NULL DEFAULT FALSE,
mc_game_resolution_x INTEGER NOT NULL DEFAULT 854,
mc_game_resolution_y INTEGER NOT NULL DEFAULT 480,
hide_on_process_start INTEGER NOT NULL DEFAULT FALSE,
hook_pre_launch TEXT NULL,
hook_wrapper TEXT NULL,
hook_post_exit TEXT NULL,
custom_dir TEXT NULL,
prev_custom_dir TEXT NULL,
migrated INTEGER NOT NULL DEFAULT FALSE,
PRIMARY KEY (id)
);
INSERT INTO settings (id, extra_launch_args, custom_env_vars) VALUES (0, jsonb_array(), jsonb_array());
CREATE TABLE java_versions (
major_version INTEGER NOT NULL,
full_version TEXT NOT NULL,
architecture TEXT NOT NULL,
path TEXT NOT NULL,
PRIMARY KEY (major_version)
);
CREATE TABLE minecraft_users (
uuid TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT FALSE,
username TEXT NOT NULL,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
expires INTEGER NOT NULL,
PRIMARY KEY (uuid)
);
CREATE UNIQUE INDEX minecraft_users_active ON minecraft_users(active);
CREATE TABLE minecraft_device_tokens (
id INTEGER NOT NULL CHECK (id = 0),
uuid TEXT NOT NULL,
private_key TEXT NOT NULL,
x TEXT NOT NULL,
y TEXT NOT NULL,
issue_instant INTEGER NOT NULL,
not_after INTEGER NOT NULL,
token TEXT NOT NULL,
display_claims JSONB NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE modrinth_users (
id TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT FALSE,
session_id TEXT NOT NULL,
expires INTEGER NOT NULL,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX modrinth_users_active ON modrinth_users(active);
CREATE TABLE cache (
id TEXT NOT NULL,
data_type TEXT NOT NULL,
alias TEXT NULL,
data JSONB NULL,
expires INTEGER NOT NULL,
UNIQUE (data_type, alias),
PRIMARY KEY (id, data_type)
);
CREATE TABLE profiles (
path TEXT NOT NULL,
install_stage TEXT NOT NULL,
name TEXT NOT NULL,
icon_path TEXT NULL,
game_version TEXT NOT NULL,
mod_loader TEXT NOT NULL,
mod_loader_version TEXT NULL,
-- array of strings
groups JSONB NOT NULL,
linked_project_id TEXT NULL,
linked_version_id TEXT NULL,
locked INTEGER NULL,
created INTEGER NOT NULL,
modified INTEGER NOT NULL,
last_played INTEGER NULL,
submitted_time_played INTEGER NOT NULL DEFAULT 0,
recent_time_played INTEGER NOT NULL DEFAULT 0,
override_java_path TEXT NULL,
-- array of strings
override_extra_launch_args JSONB NOT NULL,
-- array of (string, string)
override_custom_env_vars JSONB NOT NULL,
override_mc_memory_max INTEGER NULL,
override_mc_force_fullscreen INTEGER NULL,
override_mc_game_resolution_x INTEGER NULL,
override_mc_game_resolution_y INTEGER NULL,
override_hook_pre_launch TEXT NULL,
override_hook_wrapper TEXT NULL,
override_hook_post_exit TEXT NULL,
PRIMARY KEY (path)
);
CREATE TABLE processes (
pid INTEGER NOT NULL,
start_time INTEGER NOT NULL,
name TEXT NOT NULL,
executable TEXT NOT NULL,
profile_path TEXT NOT NULL,
post_exit_command TEXT NULL,
UNIQUE (pid),
PRIMARY KEY (pid),
FOREIGN KEY (profile_path) REFERENCES profiles(path)
);
CREATE INDEX processes_profile_path ON processes(profile_path);

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