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 .turbo
target target
generated 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' resolver = '2'
members = [ members = [
'./packages/app-lib', './packages/app-lib',
'./packages/app-macros',
'./apps/app-playground', './apps/app-playground',
'./apps/app' './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 lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size opt-level = "s" # Optimize for binary size
strip = true # Remove debug symbols strip = true # Remove debug symbols
[profile.dev.package.sqlx-macros]
opt-level = 3

View File

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

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue' import { computed, ref, onMounted } from 'vue'
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router' import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
import { import {
HomeIcon, HomeIcon,
@@ -21,11 +21,11 @@ import SplashScreen from '@/components/ui/SplashScreen.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue' import ErrorModal from '@/components/ui/ErrorModal.vue'
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator' import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
import { handleError, useNotifications } from '@/store/notifications.js' 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 { MinimizeIcon, MaximizeIcon, ChatIcon } from '@/assets/icons'
import { type } from '@tauri-apps/api/os' import { type } from '@tauri-apps/api/os'
import { appWindow } from '@tauri-apps/api/window' 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 { import {
mixpanel_track, mixpanel_track,
mixpanel_init, mixpanel_init,
@@ -36,18 +36,27 @@ import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { window as TauriWindow } from '@tauri-apps/api' import { window as TauriWindow } from '@tauri-apps/api'
import { TauriEvent } from '@tauri-apps/api/event' 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 URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue' import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
import { install_from_file } from './helpers/pack' import { install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js' 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 themeStore = useTheming()
const urlModal = ref(null) const urlModal = ref(null)
const isLoading = ref(true) 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 showOnboarding = ref(false)
const nativeDecorations = ref(false) const nativeDecorations = ref(false)
@@ -62,16 +71,16 @@ defineExpose({
const { const {
native_decorations, native_decorations,
theme, theme,
opt_out_analytics, telemetry,
collapsed_navigation, collapsed_navigation,
advanced_rendering, advanced_rendering,
fully_onboarded, onboarded,
} = await get() } = await get()
// video should play if the user is not on linux, and has not onboarded // video should play if the user is not on linux, and has not onboarded
os.value = await getOS() os.value = await getOS()
const dev = await isDev() const dev = await isDev()
const version = await getVersion() const version = await getVersion()
showOnboarding.value = !fully_onboarded showOnboarding.value = !onboarded
nativeDecorations.value = native_decorations nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') appWindow.setDecorations(native_decorations) if (os.value !== 'MacOS') appWindow.setDecorations(native_decorations)
@@ -81,10 +90,10 @@ defineExpose({
themeStore.advancedRendering = advanced_rendering themeStore.advancedRendering = advanced_rendering
mixpanel_init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' }) mixpanel_init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' })
if (opt_out_analytics) { if (telemetry) {
mixpanel_opt_out_tracking() 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()) if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
@@ -94,11 +103,6 @@ defineExpose({
document.getElementsByTagName('html')[0].classList.add('windows') document.getElementsByTagName('html')[0].classList.add('windows')
} }
offline.value = await isOffline()
await offline_listener((b) => {
offline.value = b
})
await warning_listener((e) => await warning_listener((e) =>
notificationsWrapper.value.addNotification({ notificationsWrapper.value.addNotification({
title: 'Warning', 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 () => { 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() 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 () => { TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
await handleClose() await handleClose()
}) })
@@ -179,15 +144,22 @@ const loading = useLoading()
const notifications = useNotifications() const notifications = useNotifications()
const notificationsWrapper = ref() const notificationsWrapper = ref()
watch(notificationsWrapper, () => {
notifications.setNotifs(notificationsWrapper.value)
})
const error = useError() const error = useError()
const errorModal = ref() 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) error.setErrorModal(errorModal.value)
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
install.setInstallConfirmModal(installConfirmModal)
install.setModInstallModal(modInstallModal)
}) })
document.querySelector('body').addEventListener('click', function (e) { document.querySelector('body').addEventListener('click', function (e) {
@@ -284,7 +256,7 @@ command_listener(async (e) => {
<div class="button-row push-right"> <div class="button-row push-right">
<Button @click="showLauncherLogsFolder"><FileIcon />Open launcher logs</Button> <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> </div>
</Card> </Card>
</div> </div>
@@ -385,6 +357,9 @@ command_listener(async (e) => {
<URLConfirmModal ref="urlModal" /> <URLConfirmModal ref="urlModal" />
<Notifications ref="notificationsWrapper" /> <Notifications ref="notificationsWrapper" />
<ErrorModal ref="errorModal" /> <ErrorModal ref="errorModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<InstallConfirmModal ref="installConfirmModal" />
</template> </template>
<style lang="scss" scoped> <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 { .nav-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -643,14 +569,6 @@ command_listener(async (e) => {
gap: 1rem; gap: 1rem;
} }
.video {
margin-top: 2.25rem;
width: 100vw;
height: calc(100vh - 2.25rem);
object-fit: cover;
border-radius: var(--radius-md);
}
.button-row { .button-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -127,46 +127,46 @@ const sortBy = ref('Name')
const filteredResults = computed(() => { const filteredResults = computed(() => {
let instances = props.instances.filter((instance) => { 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') { if (sortBy.value === 'Name') {
instances.sort((a, b) => { instances.sort((a, b) => {
return a.metadata.name.localeCompare(b.metadata.name) return a.name.localeCompare(b.name)
}) })
} }
if (sortBy.value === 'Game version') { if (sortBy.value === 'Game version') {
instances.sort((a, b) => { 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') { if (sortBy.value === 'Last played') {
instances.sort((a, b) => { 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') { if (sortBy.value === 'Date created') {
instances.sort((a, b) => { 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') { if (sortBy.value === 'Date modified') {
instances.sort((a, b) => { 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') { if (filters.value === 'Custom instances') {
instances = instances.filter((instance) => { instances = instances.filter((instance) => {
return !instance.metadata?.linked_data return !instance.linked_data
}) })
} else if (filters.value === 'Downloaded modpacks') { } else if (filters.value === 'Downloaded modpacks') {
instances = instances.filter((instance) => { instances = instances.filter((instance) => {
return instance.metadata?.linked_data return instance.linked_data
}) })
} }
@@ -174,7 +174,7 @@ const filteredResults = computed(() => {
if (group.value === 'Loader') { if (group.value === 'Loader') {
instances.forEach((instance) => { instances.forEach((instance) => {
const loader = formatCategoryHeader(instance.metadata.loader) const loader = formatCategoryHeader(instance.loader)
if (!instanceMap.has(loader)) { if (!instanceMap.has(loader)) {
instanceMap.set(loader, []) instanceMap.set(loader, [])
} }
@@ -183,19 +183,19 @@ const filteredResults = computed(() => {
}) })
} else if (group.value === 'Game version') { } else if (group.value === 'Game version') {
instances.forEach((instance) => { instances.forEach((instance) => {
if (!instanceMap.has(instance.metadata.game_version)) { if (!instanceMap.has(instance.game_version)) {
instanceMap.set(instance.metadata.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') { } else if (group.value === 'Category') {
instances.forEach((instance) => { instances.forEach((instance) => {
if (instance.metadata.groups.length === 0) { if (instance.groups.length === 0) {
instance.metadata.groups.push('None') instance.groups.push('None')
} }
for (const category of instance.metadata.groups) { for (const category of instance.groups) {
if (!instanceMap.has(category)) { if (!instanceMap.has(category)) {
instanceMap.set(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 { computed, onMounted, onUnmounted, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import ProjectCard from '@/components/ui/ProjectCard.vue' import ProjectCard from '@/components/ui/ProjectCard.vue'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue' import { get_by_profile_path } from '@/helpers/process.js'
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 { handleError } from '@/store/notifications.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 { useRouter } from 'vue-router'
import { showProfileInFolder } from '@/helpers/utils.js' 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 { useTheming } from '@/store/state.js'
import { mixpanel_track } from '@/helpers/mixpanel' import { mixpanel_track } from '@/helpers/mixpanel'
import { handleSevereError } from '@/store/error.js' import { handleSevereError } from '@/store/error.js'
import { install as installVersion } from '@/store/install.js'
const router = useRouter() const router = useRouter()
@@ -58,9 +51,7 @@ const modsRow = ref(null)
const instanceOptions = ref(null) const instanceOptions = ref(null)
const instanceComponents = ref(null) const instanceComponents = ref(null)
const rows = ref(null) const rows = ref(null)
const confirmModal = ref(null)
const deleteConfirmModal = ref(null) const deleteConfirmModal = ref(null)
const modInstallModal = ref(null)
const themeStore = useTheming() const themeStore = useTheming()
const currentDeleteInstance = ref(null) 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) const options =
? [ runningProcesses.length > 0
{ ? [
name: 'stop', {
color: 'danger', name: 'stop',
}, color: 'danger',
...baseOptions, },
] ...baseOptions,
: [ ]
{ : [
name: 'play', {
color: 'primary', name: 'play',
}, color: 'primary',
...baseOptions, },
] ...baseOptions,
]
instanceOptions.value.showMenu(event, passedInstance, options) instanceOptions.value.showMenu(event, passedInstance, options)
} }
@@ -132,22 +124,20 @@ const handleOptionsClick = async (args) => {
case 'play': case 'play':
await run(args.item.path).catch(handleSevereError) await run(args.item.path).catch(handleSevereError)
mixpanel_track('InstanceStart', { mixpanel_track('InstanceStart', {
loader: args.item.metadata.loader, loader: args.item.loader,
game_version: args.item.metadata.game_version, game_version: args.item.game_version,
}) })
break break
case 'stop': case 'stop':
for (const u of await get_uuids_by_profile_path(args.item.path).catch(handleError)) { await kill(args.item.path).catch(handleError)
await kill_by_uuid(u).catch(handleError)
}
mixpanel_track('InstanceStop', { mixpanel_track('InstanceStop', {
loader: args.item.metadata.loader, loader: args.item.loader,
game_version: args.item.metadata.game_version, game_version: args.item.game_version,
}) })
break break
case 'add_content': case 'add_content':
await router.push({ 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 }, query: { i: args.item.path },
}) })
break break
@@ -170,21 +160,8 @@ const handleOptionsClick = async (args) => {
await navigator.clipboard.writeText(args.item.path) await navigator.clipboard.writeText(args.item.path)
break break
case 'install': { case 'install': {
const versions = await useFetch( await installVersion(args.item.project_id, null, null, 'ProjectCardContextMenu')
`https://api.modrinth.com/v2/project/${args.item.project_id}/version`,
'project versions',
)
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 break
} }
case 'open_link': case 'open_link':
@@ -243,7 +220,7 @@ onUnmounted(() => {
<router-link :to="row.route">{{ row.label }}</router-link> <router-link :to="row.route">{{ row.label }}</router-link>
<ChevronRightIcon /> <ChevronRightIcon />
</div> </div>
<section v-if="row.instances[0].metadata" ref="modsRow" class="instances"> <section v-if="row.instance" ref="modsRow" class="instances">
<Instance <Instance
v-for="instance in row.instances.slice(0, maxInstancesPerRow)" v-for="instance in row.instances.slice(0, maxInstancesPerRow)"
:key="(instance?.project_id || instance?.id) + instance.install_stage" :key="(instance?.project_id || instance?.id) + instance.install_stage"
@@ -258,8 +235,6 @@ onUnmounted(() => {
ref="instanceComponents" ref="instanceComponents"
class="item" class="item"
:project="project" :project="project"
:confirm-modal="confirmModal"
:mod-install-modal="modInstallModal"
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)" @contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
/> />
</section> </section>
@@ -278,8 +253,6 @@ onUnmounted(() => {
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template> <template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template> <template #copy_link> <ClipboardCopyIcon /> Copy link </template>
</ContextMenu> </ContextMenu>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.content { .content {

View File

@@ -2,7 +2,7 @@
import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets' import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets'
import { Button, OverflowMenu } from '@modrinth/ui' import { Button, OverflowMenu } from '@modrinth/ui'
import { open } from '@tauri-apps/api/dialog' 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 { handleError } from '@/store/notifications.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -20,14 +20,13 @@ const handleAddContentFromFile = async () => {
if (!newProject) return if (!newProject) return
for (const project of newProject) { 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 () => { const handleSearchContent = async () => {
await router.push({ 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 }, query: { i: props.instance.path },
}) })
} }

View File

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

View File

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

View File

@@ -1,22 +1,14 @@
<script setup> <script setup>
import { onUnmounted, ref, watch } from 'vue' import { onUnmounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router' 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 { Card, Avatar, AnimatedLogo } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue' import { kill, run } from '@/helpers/profile'
import { install as pack_install } from '@/helpers/pack' import { get_by_profile_path } from '@/helpers/process'
import { list, run } from '@/helpers/profile'
import {
get_all_running_profile_paths,
get_uuids_by_profile_path,
kill_by_uuid,
} from '@/helpers/process'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import { mixpanel_track } from '@/helpers/mixpanel' import { mixpanel_track } from '@/helpers/mixpanel'
import { handleSevereError } from '@/store/error.js' 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 playing = ref(false)
const uuid = ref(null) const modLoading = computed(() => props.instance.install_stage !== 'installed')
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 router = useRouter() const router = useRouter()
const seeInstance = async () => { const seeInstance = async () => {
const instancePath = props.instance.metadata await router.push(`/instance/${encodeURIComponent(props.instance.path)}/`)
? `/instance/${encodeURIComponent(props.instance.path)}/`
: `/project/${encodeURIComponent(props.instance.project_id)}/`
await router.push(instancePath)
} }
const checkProcess = async () => { 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 = runningProcesses.length > 0
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
} }
const play = async (e, context) => { const play = async (e, context) => {
e?.stopPropagation() e?.stopPropagation()
modLoading.value = true modLoading.value = true
uuid.value = await run(props.instance.path).catch(handleSevereError) await run(props.instance.path).catch(handleSevereError)
modLoading.value = false modLoading.value = false
playing.value = true
mixpanel_track('InstancePlay', { mixpanel_track('InstancePlay', {
loader: props.instance.metadata.loader, loader: props.instance.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.game_version,
source: context, source: context,
}) })
} }
@@ -138,22 +54,13 @@ const stop = async (e, context) => {
e?.stopPropagation() e?.stopPropagation()
playing.value = false playing.value = false
// If we lost the uuid for some reason, such as a user navigating await kill(props.instance.path).catch(handleError)
// 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
mixpanel_track('InstanceStop', { mixpanel_track('InstanceStop', {
loader: props.instance.metadata.loader, loader: props.instance.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.game_version,
source: context, source: context,
}) })
uuid.value = null
} }
const openFolder = async () => { const openFolder = async () => {
@@ -162,14 +69,12 @@ const openFolder = async () => {
const addContent = async () => { const addContent = async () => {
await router.push({ 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 }, query: { i: props.instance.path },
}) })
} }
defineExpose({ defineExpose({
install,
playing,
play, play,
stop, stop,
seeInstance, seeInstance,
@@ -179,7 +84,7 @@ defineExpose({
}) })
const unlisten = await process_listener((e) => { 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()) onUnmounted(() => unlisten())
@@ -190,46 +95,32 @@ onUnmounted(() => unlisten())
<Card class="instance-card-item button-base" @click="seeInstance" @mouseenter="checkProcess"> <Card class="instance-card-item button-base" @click="seeInstance" @mouseenter="checkProcess">
<Avatar <Avatar
size="lg" size="lg"
:src=" :src="props.instance.icon_path ? convertFileSrc(props.instance.icon_path) : null"
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
"
alt="Mod card" alt="Mod card"
class="mod-image" class="mod-image"
/> />
<div class="project-info"> <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"> <p class="description">
{{ props.instance.metadata?.loader }} {{ props.instance.loader }}
{{ props.instance.metadata?.game_version || props.instance.latest_version }} {{ props.instance.game_version }}
</p> </p>
</div> </div>
</Card> </Card>
<div <div
v-if="props.instance.metadata && playing === false && modLoading === false" v-if="playing === true"
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"
class="stop cta button-base" class="stop cta button-base"
@click="(e) => stop(e, 'InstanceCard')" @click="(e) => stop(e, 'InstanceCard')"
@mousehover="checkProcess" @mousehover="checkProcess"
> >
<StopCircleIcon /> <StopCircleIcon />
</div> </div>
<div v-else class="install cta button-base" @click="install"><DownloadIcon /></div> <div v-else-if="modLoading === true && playing === false" class="cta loading-cta">
<InstallConfirmModal ref="confirmModal" /> <AnimatedLogo class="loading-indicator" />
<ModInstallModal ref="modInstallModal" /> </div>
<div v-else class="install cta button-base" @click="(e) => play(e, 'InstanceCard')">
<PlayIcon />
</div>
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ const filteredVersions = computed(() => {
}) })
const modpackVersionModal = ref(null) 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 installing = computed(() => props.instance.install_stage !== 'installed')
const inProgress = ref(false) const inProgress = ref(false)
@@ -50,7 +50,7 @@ const switchVersion = async (versionId) => {
:noblur="!themeStore.advancedRendering" :noblur="!themeStore.advancedRendering"
> >
<div class="modal-body"> <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">
<div class="table-row with-columns table-head"> <div class="table-row with-columns table-head">
<div class="table-cell table-text download-cell" /> <div class="table-cell table-text download-cell" />

View File

@@ -6,10 +6,8 @@ import { computed, ref } from 'vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useFetch } from '@/helpers/fetch.js' import { install as installVersion } from '@/store/install.js'
import { list } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js'
import { install as pack_install } from '@/helpers/pack.js'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const router = useRouter() const router = useRouter()
@@ -22,18 +20,6 @@ const props = defineProps({
return {} return {}
}, },
}, },
confirmModal: {
type: Object,
default() {
return {}
},
},
modInstallModal: {
type: Object,
default() {
return {}
},
},
}) })
const toColor = computed(() => { const toColor = computed(() => {
@@ -65,40 +51,15 @@ const toTransparent = computed(() => {
const install = async (e) => { const install = async (e) => {
e?.stopPropagation() e?.stopPropagation()
installing.value = true installing.value = true
const versions = await useFetch( await installVersion(
`https://api.modrinth.com/v2/project/${props.project.project_id}/version`, props.project.project_id,
'project versions', null,
) props.instance ? props.instance.path : null,
'ProjectCard',
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)
installing.value = false 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> </script>

View File

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

View File

@@ -69,12 +69,7 @@ import { formatNumber, formatCategory } from '@modrinth/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { ref } from 'vue' import { ref } from 'vue'
import { add_project_from_version as installMod, list } from '@/helpers/profile.js' import { install as installVersion } from '@/store/install.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'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const props = defineProps({ const props = defineProps({
@@ -94,18 +89,6 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
confirmModal: {
type: Object,
default: null,
},
modInstallModal: {
type: Object,
default: null,
},
incompatibilityWarningModal: {
type: Object,
default: null,
},
featured: { featured: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -123,93 +106,19 @@ const installed = ref(props.installed)
async function install() { async function install() {
installing.value = true installing.value = true
const versions = await useFetch( await installVersion(
`https://api.modrinth.com/v2/project/${props.project.project_id}/version`, props.project.project_id,
'project versions', null,
) props.instance ? props.instance.path : null,
let queuedVersionData 'SearchCard',
(version) => {
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,
)
installing.value = false installing.value = false
return
}
if (props.instance) installed.value = true
}
installing.value = false if (props.instance && version) {
installed.value = true
}
},
)
} }
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets' import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
import { Button, Card, Checkbox } from '@modrinth/ui' import { Button, Card, Checkbox, Modal } from '@modrinth/ui'
import { import {
DiscordIcon, DiscordIcon,
GithubIcon, GithubIcon,
@@ -9,31 +9,57 @@ import {
SteamIcon, SteamIcon,
GitLabIcon, GitLabIcon,
} from '@/assets/external' } from '@/assets/external'
import { import { login, login_2fa, create_account, login_pass } from '@/helpers/mr_auth.js'
authenticate_begin_flow,
authenticate_await_completion,
login_2fa,
create_account,
login_pass,
} from '@/helpers/mr_auth.js'
import { handleError, useNotifications } from '@/store/state.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({ const props = defineProps({
nextPage: { callback: {
type: Function, type: Function,
required: true, 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 loggingIn = ref(true)
const twoFactorFlow = ref(null) const twoFactorFlow = ref(null)
const twoFactorCode = ref('') const twoFactorCode = ref('')
@@ -45,22 +71,13 @@ const confirmPassword = ref('')
const subscribe = ref(true) const subscribe = ref(true)
async function signInOauth(provider) { async function signInOauth(provider) {
const url = await authenticate_begin_flow(provider).catch(handleError) const creds = await login(provider).catch(handleSevereError)
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: url,
},
})
const creds = await authenticate_await_completion().catch(handleError)
if (creds && creds.type === 'two_factor_required') { if (creds && creds.type === 'two_factor_required') {
twoFactorFlow.value = creds.flow twoFactorFlow.value = creds.flow
} else if (creds && creds.session) { } 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) const creds = await login_2fa(twoFactorCode.value, twoFactorFlow.value).catch(handleError)
if (creds && creds.session) { if (creds && creds.session) {
props.nextPage() props.callback()
modal.value.hide()
} }
} }
async function signIn() { async function signIn() {
const creds = await login_pass( const creds = await login_pass(username.value, password.value, turnstileToken.value).catch(
username.value, handleError,
password.value, )
window.turnstile.getResponse(), window.turnstile.reset(widgetId.value)
).catch(handleError)
window.turnstile.reset()
if (creds && creds.type === 'two_factor_required') { if (creds && creds.type === 'two_factor_required') {
twoFactorFlow.value = creds.flow twoFactorFlow.value = creds.flow
} else if (creds && creds.session) { } else if (creds && creds.session) {
props.nextPage() props.callback()
modal.value.hide()
} }
} }
@@ -102,117 +119,128 @@ async function createAccount() {
username.value, username.value,
email.value, email.value,
password.value, password.value,
window.turnstile.getResponse(), turnstileToken.value,
subscribe.value, subscribe.value,
).catch(handleError) ).catch(handleError)
window.turnstile.reset() window.turnstile.reset(widgetId.value)
if (creds && creds.session) { 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> </script>
<template> <template>
<Card> <Modal ref="modal" :on-hide="removeWidget">
<div class="cf-turnstile" data-sitekey="0x4AAAAAAAHWfmKCm7cUG869"></div> <Card>
<template v-if="twoFactorFlow"> <template v-if="twoFactorFlow">
<h1>Enter two-factor code</h1> <h1>Enter two-factor code</h1>
<p>Please enter a two-factor code to proceed.</p> <p>Please enter a two-factor code to proceed.</p>
<input v-model="twoFactorCode" maxlength="11" type="text" placeholder="Enter code..." /> <input v-model="twoFactorCode" maxlength="11" type="text" placeholder="Enter code..." />
</template> </template>
<template v-else> <template v-else>
<h1 v-if="loggingIn">Login to Modrinth</h1> <h1 v-if="loggingIn">Login to Modrinth</h1>
<h1 v-else>Create an account</h1> <h1 v-else>Create an account</h1>
<div class="button-grid"> <div class="button-grid">
<Button class="discord" large @click="signInOauth('discord')"> <Button class="discord" large @click="signInOauth('discord')">
<DiscordIcon /> <DiscordIcon />
Discord Discord
</Button> </Button>
<Button class="github" large @click="signInOauth('github')"> <Button class="github" large @click="signInOauth('github')">
<GithubIcon /> <GithubIcon />
Github Github
</Button> </Button>
<Button class="white" large @click="signInOauth('microsoft')"> <Button class="white" large @click="signInOauth('microsoft')">
<MicrosoftIcon /> <MicrosoftIcon />
Microsoft Microsoft
</Button> </Button>
<Button class="google" large @click="signInOauth('google')"> <Button class="google" large @click="signInOauth('google')">
<GoogleIcon /> <GoogleIcon />
Google Google
</Button> </Button>
<Button class="white" large @click="signInOauth('steam')"> <Button class="white" large @click="signInOauth('steam')">
<SteamIcon /> <SteamIcon />
Steam Steam
</Button> </Button>
<Button class="gitlab" large @click="signInOauth('gitlab')"> <Button class="gitlab" large @click="signInOauth('gitlab')">
<GitLabIcon /> <GitLabIcon />
GitLab GitLab
</Button> </Button>
</div> </div>
<div class="divider"> <div class="divider">
<hr /> <hr />
<p>Or</p> <p>Or</p>
</div> </div>
<div v-if="!loggingIn" class="iconified-input username"> <div v-if="!loggingIn" class="iconified-input username">
<MailIcon /> <MailIcon />
<input v-model="email" type="text" placeholder="Email" /> <input v-model="email" type="text" placeholder="Email" />
</div> </div>
<div class="iconified-input username"> <div class="iconified-input username">
<UserIcon /> <UserIcon />
<input <input
v-model="username" v-model="username"
type="text" type="text"
:placeholder="loggingIn ? 'Email or username' : 'Username'" :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>
<div class="iconified-input" :class="{ username: !loggingIn }"> </Card>
<LockIcon /> </Modal>
<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>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
:deep(.modal-container) {
.modal-body {
width: auto;
.content {
background: none;
}
}
}
.card { .card {
width: 25rem; width: 25rem;
} }
@@ -321,4 +349,19 @@ onMounted(() => {
:deep(.checkbox) { :deep(.checkbox) {
border: none; 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> </style>

View File

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

View File

@@ -45,11 +45,3 @@ export async function remove_user(user) {
export async function users() { export async function users() {
return await invoke('plugin:auth|auth_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

@@ -93,15 +93,3 @@ export async function command_listener(callback) {
export async function warning_listener(callback) { export async function warning_listener(callback) {
return await listen('warning', (event) => callback(event.payload)) 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 // Finds all the installation of Java 7, if it exists
// Returns [JavaVersion] // Returns [JavaVersion]
export async function find_filtered_jres(version) { 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') return await invoke('plugin:metadata|metadata_get_game_versions')
} }
// Gets the fabric versions from daedalus // Gets the given loader versions from daedalus
// Returns Manifest // Returns Manifest
export async function get_fabric_versions() { export async function get_loader_versions(loader) {
const c = await invoke('plugin:metadata|metadata_get_fabric_versions') return await invoke('plugin:metadata|metadata_get_loader_versions', { loader })
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
} }

View File

@@ -5,17 +5,10 @@
*/ */
import { invoke } from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
export async function authenticate_begin_flow(provider) { export async function login(provider) {
return await invoke('plugin:mr_auth|authenticate_begin_flow', { 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) { export async function login_pass(username, password, challenge) {
return await invoke('plugin:mr_auth|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() { export async function logout() {
return await invoke('plugin:mr_auth|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.gameVersion,
profile_creator.modloader, profile_creator.modloader,
profile_creator.loaderVersion, profile_creator.loaderVersion,
profile_creator.icon, null,
true,
) )
return await invoke('plugin:pack|pack_install', { location, profile }) 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.gameVersion,
profile_creator.modloader, profile_creator.modloader,
profile_creator.loaderVersion, profile_creator.loaderVersion,
profile_creator.icon, null,
true,
) )
return await invoke('plugin:pack|pack_install', { location, profile }) return await invoke('plugin:pack|pack_install', { location, profile })
} }

View File

@@ -5,49 +5,19 @@
*/ */
import { invoke } from '@tauri-apps/api/tauri' import { invoke } from '@tauri-apps/api/tauri'
/// Gets if a process has finished by UUID /// Gets all running process IDs with a given profile path
/// 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
/// Returns [u32] /// Returns [u32]
export async function get_all_uuids() { export async function get_by_profile_path(path) {
return await invoke('plugin:process|process_get_all_uuids') return await invoke('plugin:process|process_get_by_profile_path', { path })
}
/// Gets all running process IDs
/// Returns [u32]
export async function get_all_running_uuids() {
return await invoke('plugin:process|process_get_all_running_uuids')
} }
/// Gets all running process IDs with a given profile path /// Gets all running process IDs with a given profile path
/// Returns [u32] /// Returns [u32]
export async function get_uuids_by_profile_path(profilePath) { export async function get_all() {
return await invoke('plugin:process|process_get_uuids_by_profile_path', { profilePath }) return await invoke('plugin:process|process_get_all')
}
/// 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')
} }
/// Kills a process by UUID /// Kills a process by UUID
export async function kill_by_uuid(uuid) { export async function kill(pid) {
return await invoke('plugin:process|process_kill_by_uuid', { uuid }) 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 - 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" //Trim string name to avoid "Unable to find directory"
name = name.trim() name = name.trim()
return await invoke('plugin:profile_create|profile_create', { return await invoke('plugin:profile_create|profile_create', {
@@ -24,8 +24,8 @@ export async function create(name, gameVersion, modloader, loaderVersion, icon,
gameVersion, gameVersion,
modloader, modloader,
loaderVersion, loaderVersion,
icon, iconPath,
noWatch, skipInstall,
}) })
} }
@@ -41,8 +41,18 @@ export async function remove(path) {
// Get a profile by path // Get a profile by path
// Returns a Profile // Returns a Profile
export async function get(path, clearProjects) { export async function get(path) {
return await invoke('plugin:profile|profile_get', { path, clearProjects }) 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 // 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 // Get a copy of the profile set
// Returns hashmap of path -> Profile // Returns hashmap of path -> Profile
export async function list(clearProjects) { export async function list() {
return await invoke('plugin:profile|profile_list', { clearProjects }) return await invoke('plugin:profile|profile_list')
} }
export async function check_installed(path, projectId) { export async function check_installed(path, projectId) {
@@ -163,10 +173,8 @@ export async function run(path) {
return await invoke('plugin:profile|profile_run', { path }) return await invoke('plugin:profile|profile_run', { path })
} }
// Run Minecraft using a pathed profile export async function kill(path) {
// Waits for end return await invoke('plugin:profile|profile_kill', { path })
export async function run_wait(path) {
return await invoke('plugin:profile|profile_run_wait', { path })
} }
// Edits a profile // Edits a profile

View File

@@ -16,11 +16,6 @@ export async function progress_bars_list() {
return await invoke('plugin:utils|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 // Get opening command
// For example, if a user clicks on an .mrpack to open the app. // 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 // 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() { export async function get_opening_command() {
return await invoke('plugin:utils|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' 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 // Gets cached category tags
export async function get_categories() { export async function get_categories() {
return await invoke('plugin:tags|tags_get_categories') return await invoke('plugin:tags|tags_get_categories')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,26 +4,17 @@
<Card v-if="instance" class="small-instance"> <Card v-if="instance" class="small-instance">
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`"> <router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
<Avatar <Avatar
:src=" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
!instance.metadata.icon || :alt="instance.name"
(instance.metadata.icon && instance.metadata.icon.startsWith('http'))
? instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
:alt="instance.metadata.name"
size="sm" size="sm"
/> />
<div class="small-instance_info"> <div class="small-instance_info">
<span class="title">{{ <span class="title">{{
instance.metadata.name.length > 20 instance.name.length > 20 ? instance.name.substring(0, 20) + '...' : instance.name
? instance.metadata.name.substring(0, 20) + '...'
: instance.metadata.name
}}</span> }}</span>
<span> <span>
{{ {{ instance.loader.charAt(0).toUpperCase() + instance.loader.slice(1) }}
instance.metadata.loader.charAt(0).toUpperCase() + instance.metadata.loader.slice(1) {{ instance.game_version }}
}}
{{ instance.metadata.game_version }}
</span> </span>
</div> </div>
</router-link> </router-link>
@@ -209,7 +200,6 @@
:project="data" :project="data"
:versions="versions" :versions="versions"
:members="members" :members="members"
:dependencies="dependencies"
:instance="instance" :instance="instance"
:install="install" :install="install"
:installed="installed" :installed="installed"
@@ -218,9 +208,6 @@
/> />
</div> </div>
</div> </div>
<InstallConfirmModal ref="confirmModal" />
<ModInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarning" />
<ContextMenu ref="options" @option-clicked="handleOptionsClick"> <ContextMenu ref="options" @option-clicked="handleOptionsClick">
<template #install> <DownloadIcon /> Install </template> <template #install> <DownloadIcon /> Install </template>
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template> <template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
@@ -263,79 +250,63 @@ import {
OpenCollectiveIcon, OpenCollectiveIcon,
} from '@/assets/external' } from '@/assets/external'
import { get_categories } from '@/helpers/tags' import { get_categories } from '@/helpers/tags'
import { install as packInstall } from '@/helpers/pack' import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
import {
list,
add_project_from_version as installMod,
check_installed,
get as getInstance,
remove_project,
} from '@/helpers/profile'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ref, shallowRef, watch } from 'vue' 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 { useBreadcrumbs } from '@/store/breadcrumbs'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import ContextMenu from '@/components/ui/ContextMenu.vue' 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 route = useRoute()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarning = ref(null)
const options = ref(null) const options = ref(null)
const installing = ref(false) const installing = ref(false)
const data = shallowRef(null) const data = shallowRef(null)
const versions = shallowRef([]) const versions = shallowRef([])
const members = shallowRef([]) const members = shallowRef([])
const dependencies = shallowRef([])
const categories = shallowRef([]) const categories = shallowRef([])
const instance = ref(null) const instance = ref(null)
const instanceProjects = ref(null)
const installed = ref(false) const installed = ref(false)
const installedVersion = ref(null) const installedVersion = ref(null)
const offline = ref(await isOffline())
async function fetchProjectData() { async function fetchProjectData() {
;[ const project = await get_project(route.params.id).catch(handleError)
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(),
])
installed.value = data.value = project
instance.value?.path && ;[versions.value, members.value, categories.value, instance.value, instanceProjects.value] =
(await check_installed(instance.value.path, data.value.id).catch(handleError)) 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) 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( watch(
() => route.params.id, () => route.params.id,
@@ -346,162 +317,22 @@ watch(
}, },
) )
dayjs.extend(relativeTime)
const markInstalled = () => {
installed.value = true
}
async function install(version) { async function install(version) {
installing.value = true installing.value = true
let queuedVersionData await installVersion(
if (instance.value) { data.value.id,
instance.value = await getInstance(instance.value.path, false).catch(handleError) version,
} instance.value ? instance.value.path : null,
'ProjectPage',
if (installed.value) { (version) => {
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
installing.value = false installing.value = false
return
}
await remove_project(instance.value.path, old_project.key) if (instance.value && version) {
} installed.value = true
installedVersion.value = version
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
}
} }
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) => { const handleRightClick = (e) => {

View File

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

View File

@@ -187,8 +187,8 @@ const props = defineProps({
}) })
const filterVersions = ref([]) const filterVersions = ref([])
const filterLoader = ref(props.instance ? [props.instance?.metadata?.loader] : []) const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
const filterGameVersions = ref(props.instance ? [props.instance?.metadata?.game_version] : []) const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
const currentPage = ref(1) 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 { useBreadcrumbs } from './breadcrumbs'
import { useLoading } from './loading' import { useLoading } from './loading'
import { useNotifications, handleError } from './notifications' 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"] } tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3" futures = "0.3"
daedalus = {version = "0.1.15", features = ["bincode"] }
uuid = { version = "1.1", features = ["serde", "v4"] } uuid = { version = "1.1", features = ["serde", "v4"] }
tracing = "0.1.37" tracing = "0.1.37"

View File

@@ -3,6 +3,8 @@
windows_subsystem = "windows" 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::prelude::*;
use theseus::profile::create::profile_create; use theseus::profile::create::profile_create;
@@ -39,21 +41,12 @@ async fn main() -> theseus::Result<()> {
let _log_guard = theseus::start_logger(); let _log_guard = theseus::start_logger();
// Initialize state // Initialize state
let st = State::get().await?; State::init().await?;
//State::update();
if minecraft_auth::users().await?.is_empty() { if minecraft_auth::users().await?.is_empty() {
println!("No users found, authenticating."); println!("No users found, authenticating.");
authenticate_run().await?; // could take credentials from here direct, but also deposited in state users 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 // st.settings
// .write() // .write()
@@ -63,56 +56,59 @@ async fn main() -> theseus::Result<()> {
// Clear profiles // Clear profiles
println!("Clearing profiles."); println!("Clearing profiles.");
{ {
let h = profile::list(None).await?; let h = profile::list().await?;
for (path, _) in h.into_iter() { for profile in h.into_iter() {
profile::remove(&path).await?; profile::remove(&profile.path).await?;
} }
} }
println!("Creating/adding profile."); println!("Creating/adding profile.");
let name = "Example".to_string(); // let name = "Example".to_string();
let game_version = "1.19.2".to_string(); // let game_version = "1.21".to_string();
let modloader = ModLoader::Vanilla; // let modloader = ModLoader::Fabric;
let loader_version = "stable".to_string(); // 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( let profile_path = profile_create(
name.clone(), profile.name,
game_version, profile.game_version,
modloader, profile.modloader,
Some(loader_version), profile.loader_version,
None,
None,
None, None,
None, None,
None, None,
) )
.await?; .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"); println!("running");
// Run a profile, running minecraft and store the RwLock to the process // Run a profile, running minecraft and store the RwLock to the process
let proc_lock = profile::run(&profile_path).await?; let process = profile::run(&profile_path).await?;
let uuid = proc_lock.read().await.uuid;
let pid = proc_lock.read().await.current_child.read().await.id();
println!("Minecraft UUID: {}", uuid); println!("Minecraft PID: {}", process.pid);
println!("Minecraft PID: {:?}", pid);
println!( println!("All running process UUID {:?}", process::get_all().await?);
"All running process UUID {:?}",
process::get_all_running_uuids().await?
);
println!(
"All running process paths {:?}",
process::get_all_running_profile_paths().await?
);
// hold the lock to the process until it ends // hold the lock to the process until it ends
println!("Waiting for process to end..."); println!("Waiting for process to end...");
let mut proc = proc_lock.write().await; process.wait_for().await?;
process::wait_for(&mut proc).await?;
Ok(()) Ok(())
} }

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
auth_set_default_user, auth_set_default_user,
auth_remove_user, auth_remove_user,
auth_users, auth_users,
auth_get_user,
]) ])
.build() .build()
} }
@@ -96,11 +95,3 @@ pub async fn auth_set_default_user(user: uuid::Uuid) -> Result<()> {
pub async fn auth_users() -> Result<Vec<Credentials>> { pub async fn auth_users() -> Result<Vec<Credentials>> {
Ok(minecraft_auth::users().await?) 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::ImportLauncherType;
use theseus::pack::import; use theseus::pack::import;
use theseus::prelude::ProfilePathId;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("import") 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") /// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1")
#[tauri::command] #[tauri::command]
pub async fn import_import_instance( pub async fn import_import_instance(
profile_path: ProfilePathId, profile_path: &str,
launcher_type: ImportLauncherType, launcher_type: ImportLauncherType,
base_path: PathBuf, base_path: PathBuf,
instance_folder: String, instance_folder: String,

View File

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

View File

@@ -1,9 +1,6 @@
use crate::api::Result; use crate::api::Result;
use theseus::logs::LogType; use theseus::logs::LogType;
use theseus::{ use theseus::logs::{self, CensoredString, LatestLogCursor, Logs};
logs::{self, CensoredString, LatestLogCursor, Logs},
prelude::ProfilePathId,
};
/* /*
A log is a struct containing the filename string, stdout, and stderr, as follows: 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 /// Get all Logs for a profile, sorted by filename
#[tauri::command] #[tauri::command]
pub async fn logs_get_logs( pub async fn logs_get_logs(
profile_path: ProfilePathId, profile_path: &str,
clear_contents: Option<bool>, clear_contents: Option<bool>,
) -> Result<Vec<Logs>> { ) -> Result<Vec<Logs>> {
let val = logs::get_logs(profile_path, clear_contents).await?; 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 /// Get a Log struct for a profile by profile id and filename string
#[tauri::command] #[tauri::command]
pub async fn logs_get_logs_by_filename( pub async fn logs_get_logs_by_filename(
profile_path: ProfilePathId, profile_path: &str,
log_type: LogType, log_type: LogType,
filename: String, filename: String,
) -> Result<Logs> { ) -> 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 /// Get the stdout for a profile by profile id and filename string
#[tauri::command] #[tauri::command]
pub async fn logs_get_output_by_filename( pub async fn logs_get_output_by_filename(
profile_path: ProfilePathId, profile_path: &str,
log_type: LogType, log_type: LogType,
filename: String, filename: String,
) -> Result<CensoredString> { ) -> Result<CensoredString> {
let profile_path = if let Some(p) = Ok(logs::get_output_by_filename(profile_path, log_type, &filename).await?)
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?,
)
} }
/// Delete all logs for a profile by profile id /// Delete all logs for a profile by profile id
#[tauri::command] #[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?) Ok(logs::delete_logs(profile_path).await?)
} }
/// Delete a log for a profile by profile id and filename string /// Delete a log for a profile by profile id and filename string
#[tauri::command] #[tauri::command]
pub async fn logs_delete_logs_by_filename( pub async fn logs_delete_logs_by_filename(
profile_path: ProfilePathId, profile_path: &str,
log_type: LogType, log_type: LogType,
filename: String, filename: String,
) -> Result<()> { ) -> Result<()> {
@@ -95,7 +78,7 @@ pub async fn logs_delete_logs_by_filename(
/// Get live log from a cursor /// Get live log from a cursor
#[tauri::command] #[tauri::command]
pub async fn logs_get_latest_log_cursor( pub async fn logs_get_latest_log_cursor(
profile_path: ProfilePathId, profile_path: &str,
cursor: u64, // 0 to start at beginning of file cursor: u64, // 0 to start at beginning of file
) -> Result<LatestLogCursor> { ) -> Result<LatestLogCursor> {
Ok(logs::get_latest_log_cursor(profile_path, cursor).await?) 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") tauri::plugin::Builder::new("metadata")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
metadata_get_game_versions, metadata_get_game_versions,
metadata_get_fabric_versions, metadata_get_loader_versions,
metadata_get_forge_versions,
metadata_get_quilt_versions,
metadata_get_neoforge_versions,
]) ])
.build() .build()
} }
@@ -22,24 +19,6 @@ pub async fn metadata_get_game_versions() -> Result<VersionManifest> {
/// Gets the fabric versions from daedalus /// Gets the fabric versions from daedalus
#[tauri::command] #[tauri::command]
pub async fn metadata_get_fabric_versions() -> Result<Manifest> { pub async fn metadata_get_loader_versions(loader: &str) -> Result<Manifest> {
Ok(theseus::metadata::get_fabric_versions().await?) Ok(theseus::metadata::get_loader_versions(loader).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?)
} }

View File

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

View File

@@ -1,17 +1,15 @@
use crate::api::Result; use crate::api::Result;
use chrono::{Duration, Utc};
use tauri::plugin::TauriPlugin; use tauri::plugin::TauriPlugin;
use tauri::{Manager, UserAttentionType};
use theseus::prelude::*; use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("mr_auth") tauri::plugin::Builder::new("mr_auth")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
authenticate_begin_flow,
authenticate_await_completion,
cancel_flow,
login_pass, login_pass,
login_2fa, login_2fa,
create_account, create_account,
refresh,
logout, logout,
get, get,
]) ])
@@ -19,19 +17,68 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
} }
#[tauri::command] #[tauri::command]
pub async fn authenticate_begin_flow(provider: &str) -> Result<String> { pub async fn modrinth_auth_login(
Ok(theseus::mr_auth::authenticate_begin_flow(provider).await?) app: tauri::AppHandle,
} provider: &str,
) -> Result<Option<ModrinthCredentialsResult>> {
let redirect_uri = mr_auth::authenticate_begin_flow(provider);
#[tauri::command] let start = Utc::now();
pub async fn authenticate_await_completion() -> Result<ModrinthCredentialsResult>
{
Ok(theseus::mr_auth::authenticate_await_complete_flow().await?)
}
#[tauri::command] if let Some(window) = app.get_window("modrinth-signin") {
pub async fn cancel_flow() -> Result<()> { window.close()?;
Ok(theseus::mr_auth::cancel_flow().await?) }
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] #[tauri::command]
@@ -66,11 +113,6 @@ pub async fn create_account(
.await?) .await?)
} }
#[tauri::command]
pub async fn refresh() -> Result<()> {
Ok(theseus::mr_auth::refresh().await?)
}
#[tauri::command] #[tauri::command]
pub async fn logout() -> Result<()> { pub async fn logout() -> Result<()> {
Ok(theseus::mr_auth::logout().await?) Ok(theseus::mr_auth::logout().await?)

View File

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

View File

@@ -1,78 +1,33 @@
use crate::api::Result; use crate::api::Result;
use theseus::prelude::*; use theseus::prelude::*;
use uuid::Uuid;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("process") tauri::plugin::Builder::new("process")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
process_has_finished_by_uuid, process_get_all,
process_get_exit_status_by_uuid, process_get_by_profile_path,
process_get_all_uuids, process_kill,
process_get_all_running_uuids, process_wait_for,
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,
]) ])
.build() .build()
} }
// Checks if a process has finished by process UUID
#[tauri::command] #[tauri::command]
pub async fn process_has_finished_by_uuid(uuid: Uuid) -> Result<bool> { pub async fn process_get_all() -> Result<Vec<Process>> {
Ok(process::has_finished_by_uuid(uuid).await?) Ok(process::get_all().await?)
} }
// Gets process exit status by process UUID
#[tauri::command] #[tauri::command]
pub async fn process_get_exit_status_by_uuid( pub async fn process_get_by_profile_path(path: &str) -> Result<Vec<Process>> {
uuid: Uuid, Ok(process::get_by_profile_path(path).await?)
) -> Result<Option<i32>> {
Ok(process::get_exit_status_by_uuid(uuid).await?)
} }
// Gets all process UUIDs
#[tauri::command] #[tauri::command]
pub async fn process_get_all_uuids() -> Result<Vec<Uuid>> { pub async fn process_kill(pid: i32) -> Result<()> {
Ok(process::get_all_uuids().await?) Ok(process::kill(pid).await?)
} }
// Gets all running process UUIDs
#[tauri::command] #[tauri::command]
pub async fn process_get_all_running_uuids() -> Result<Vec<Uuid>> { pub async fn process_wait_for(pid: i32) -> Result<()> {
Ok(process::get_all_running_uuids().await?) Ok(process::wait_for(pid).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?)
} }

View File

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

View File

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

View File

@@ -1,16 +1,9 @@
use std::path::PathBuf;
use crate::api::Result; use crate::api::Result;
use theseus::prelude::*; use theseus::prelude::*;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("settings") tauri::plugin::Builder::new("settings")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![settings_get, settings_set])
settings_get,
settings_set,
settings_change_config_dir,
settings_is_dir_writeable
])
.build() .build()
} }
@@ -29,20 +22,3 @@ pub async fn settings_set(settings: Settings) -> Result<()> {
settings::set(settings).await?; settings::set(settings).await?;
Ok(()) 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 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> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("tags") tauri::plugin::Builder::new("tags")
@@ -9,7 +9,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tags_get_loaders, tags_get_loaders,
tags_get_game_versions, tags_get_game_versions,
tags_get_donation_platforms, tags_get_donation_platforms,
tags_get_tag_bundle,
]) ])
.build() .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>> { pub async fn tags_get_donation_platforms() -> Result<Vec<DonationPlatform>> {
Ok(theseus::tags::get_donation_platform_tags().await?) 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::{ use theseus::{
handler, handler,
prelude::{CommandPayload, DirectoryInfo}, prelude::{CommandPayload, DirectoryInfo},
State,
}; };
use crate::api::Result; use crate::api::Result;
@@ -16,11 +15,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
show_in_folder, show_in_folder,
show_launcher_logs_folder, show_launcher_logs_folder,
progress_bars_list, progress_bars_list,
safety_check_safe_loading_bars, get_opening_command
get_opening_command,
await_sync,
is_offline,
refresh_offline
]) ])
.build() .build()
} }
@@ -54,12 +49,6 @@ pub async fn progress_bars_list(
Ok(res) 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 // cfg only on mac os
// disables mouseover and fixes a random crash error only fixed by recent versions of macos // disables mouseover and fixes a random crash error only fixed by recent versions of macos
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -82,7 +71,7 @@ pub async fn should_disable_mouseover() -> bool {
} }
#[tauri::command] #[tauri::command]
pub fn show_in_folder(mut path: PathBuf) -> Result<()> { pub fn show_in_folder(path: PathBuf) -> Result<()> {
{ {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@@ -101,6 +90,7 @@ pub fn show_in_folder(mut path: PathBuf) -> Result<()> {
{ {
use std::fs::metadata; use std::fs::metadata;
let mut path = path;
let path_string = path.to_string_lossy().to_string(); let path_string = path.to_string_lossy().to_string();
if metadata(&path)?.is_dir() { 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<()> { pub async fn handle_command(command: String) -> Result<()> {
Ok(theseus::handler::parse_and_emit_command(&command).await?) 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] #[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> { async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
theseus::EventState::init(app).await?; theseus::EventState::init(app).await?;
let s = State::get().await?; State::init().await?;
State::update();
s.children.write().await.rescue_cache().await?;
Ok(()) Ok(())
} }
@@ -50,7 +48,7 @@ struct Payload {
// if Tauri app is called with arguments, then those arguments will be treated as commands // if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks // ie: deep links or filepaths for .mrpacks
fn main() { 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 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 let builder = builder
.plugin(api::auth::init()) .plugin(api::auth::init())
.plugin(api::mr_auth::init()) .plugin(api::mr_auth::init())
@@ -142,11 +141,13 @@ fn main() {
.plugin(api::settings::init()) .plugin(api::settings::init())
.plugin(api::tags::init()) .plugin(api::tags::init())
.plugin(api::utils::init()) .plugin(api::utils::init())
.plugin(api::cache::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
initialize_state, initialize_state,
is_dev, is_dev,
toggle_decorations, toggle_decorations,
api::auth::auth_login, api::auth::auth_login,
api::mr_auth::modrinth_auth_login,
]); ]);
builder builder

View File

@@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "Modrinth App", "productName": "Modrinth App",
"version": "0.7.2" "version": "0.8.0-1"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
@@ -20,8 +20,8 @@
"asset": true, "asset": true,
"assetScope": [ "assetScope": [
"$APPDATA/caches/icons/*", "$APPDATA/caches/icons/*",
"$APPCONFIG/caches/icons/*", "$APPDATA/caches/icons/*",
"$CONFIG/caches/icons/*" "$APPDATA/caches/icons/*"
] ]
}, },
"shell": { "shell": {
@@ -56,7 +56,7 @@
}, },
"externalBin": [], "externalBin": [],
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"identifier": "com.modrinth.theseus", "identifier": "ModrinthApp",
"longDescription": "", "longDescription": "",
"macOS": { "macOS": {
"entitlements": "App.entitlements", "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] [package]
name = "theseus" name = "theseus"
version = "0.7.2" version = "0.0.0"
authors = ["Jai A <jaiagr+gpg@pm.me>"] authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018" edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
theseus_macros = { path = "../app-macros" }
bytes = "1" bytes = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
@@ -23,10 +21,10 @@ async_zip = { version = "0.0.17", features = ["full"] }
flate2 = "1.0.28" flate2 = "1.0.28"
tempfile = "3.5.0" tempfile = "3.5.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
dashmap = { version = "6.0.1", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }
daedalus = { version = "0.1.25" } daedalus = { version = "0.2.2" }
dirs = "5.0.1" dirs = "5.0.1"
regex = "1.5" regex = "1.5"
@@ -59,13 +57,18 @@ dunce = "1.0.3"
whoami = "1.4.0" whoami = "1.4.0"
discord-rich-presence = "0.2.3" discord-rich-presence = "0.2.4"
p256 = { version = "0.13.2", features = ["ecdsa"] } p256 = { version = "0.13.2", features = ["ecdsa"] }
rand = "0.8" rand = "0.8"
byteorder = "1.5.0" byteorder = "1.5.0"
base64 = "0.22.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] [target.'cfg(windows)'.dependencies]
winreg = "0.52.0" 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