More app fixes 0.9.0 (#3054)
* initial set of fixes (toggle sidebar, profile pagination) * more fixes, bump version * fix lint: * fix quick switcher ordering
This commit is contained in:
parent
ef08d8e538
commit
cae6f12ea0
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -8956,7 +8956,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "0.9.0-1"
|
version = "0.9.0-2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
@ -9007,7 +9007,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.9.0-1"
|
version = "0.9.0-2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"cocoa 0.25.0",
|
"cocoa 0.25.0",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app-frontend",
|
"name": "@modrinth/app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.0-1",
|
"version": "0.9.0-2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
MaximizeIcon,
|
MaximizeIcon,
|
||||||
RestoreIcon,
|
RestoreIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
|
RightArrowIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
|
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
|
||||||
import { useLoading, useTheming } from '@/store/state'
|
import { useLoading, useTheming } from '@/store/state'
|
||||||
@ -54,40 +55,14 @@ import { get_user } from '@/helpers/cache.js'
|
|||||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
||||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const news = ref([
|
const news = ref([])
|
||||||
{
|
|
||||||
title: 'Introducing Modrinth Servers',
|
|
||||||
summary: 'Host your next Minecraft server with Modrinth.',
|
|
||||||
thumbnail:
|
|
||||||
'https://media.beehiiv.com/cdn-cgi/image/format=auto,width=800,height=421,fit=scale-down,onerror=redirect/uploads/asset/file/eefddc59-b4c4-4e7d-92e8-c26bdef42984/Modrinth-Servers-Thumb.png',
|
|
||||||
date: '2024-11-02T00:00:00Z',
|
|
||||||
link: 'https://blog.modrinth.com/p/modrinth-servers-beta',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Becoming Sustainable',
|
|
||||||
summary: 'Announcing 5x creator revenue and updates to the monetization program.',
|
|
||||||
thumbnail:
|
|
||||||
'https://media.beehiiv.com/cdn-cgi/image/format=auto,width=800,height=421,fit=scale-down,onerror=redirect/uploads/asset/file/c99b9885-8248-4d7a-b19a-3ae2c902fdd5/revenue.png',
|
|
||||||
date: '2024-09-13T00:00:00Z',
|
|
||||||
link: 'https://blog.modrinth.com/p/creator-revenue-update',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Modrinth+ and New Ads',
|
|
||||||
summary:
|
|
||||||
'Introducing a new advertising system, a subscription to remove ads, and a redesign of the website!\n',
|
|
||||||
thumbnail:
|
|
||||||
'https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/38ce85e4-5d93-43eb-b61b-b6296f6b9e66/things.png?t=1724260059',
|
|
||||||
date: '2024-08-21T00:00:00Z',
|
|
||||||
link: 'https://blog.modrinth.com/p/introducing-modrinth-refreshed-site-look-new-advertising-system',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const urlModal = ref(null)
|
const urlModal = ref(null)
|
||||||
|
|
||||||
@ -132,6 +107,9 @@ async function setupApp() {
|
|||||||
advanced_rendering,
|
advanced_rendering,
|
||||||
onboarded,
|
onboarded,
|
||||||
default_page,
|
default_page,
|
||||||
|
toggle_sidebar,
|
||||||
|
developer_mode,
|
||||||
|
feature_flags,
|
||||||
} = await get()
|
} = await get()
|
||||||
|
|
||||||
if (default_page === 'Library') {
|
if (default_page === 'Library') {
|
||||||
@ -149,6 +127,9 @@ async function setupApp() {
|
|||||||
themeStore.setThemeState(theme)
|
themeStore.setThemeState(theme)
|
||||||
themeStore.collapsedNavigation = collapsed_navigation
|
themeStore.collapsedNavigation = collapsed_navigation
|
||||||
themeStore.advancedRendering = advanced_rendering
|
themeStore.advancedRendering = advanced_rendering
|
||||||
|
themeStore.toggleSidebar = toggle_sidebar
|
||||||
|
themeStore.devMode = developer_mode
|
||||||
|
themeStore.featureFlags = feature_flags
|
||||||
|
|
||||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||||
|
|
||||||
@ -190,6 +171,12 @@ async function setupApp() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useFetch(`https://modrinth.com/blog/news.json`, 'news', true).then((res) => {
|
||||||
|
if (res && res.articles) {
|
||||||
|
news.value = res.articles
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
get_opening_command().then(handleCommand)
|
get_opening_command().then(handleCommand)
|
||||||
checkUpdates()
|
checkUpdates()
|
||||||
fetchCredentials()
|
fetchCredentials()
|
||||||
@ -263,13 +250,29 @@ const hasPlus = computed(
|
|||||||
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
|
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const sidebarToggled = ref(true)
|
||||||
|
|
||||||
|
themeStore.$subscribe(() => {
|
||||||
|
sidebarToggled.value = !themeStore.toggleSidebar
|
||||||
|
})
|
||||||
|
|
||||||
|
const forceSidebar = ref(false)
|
||||||
|
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
||||||
|
const showAd = computed(() => !(!sidebarVisible.value || hasPlus.value))
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
forceSidebar.value = to.path.startsWith('/browse') || to.path.startsWith('/project')
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
hasPlus,
|
showAd,
|
||||||
() => {
|
() => {
|
||||||
if (hasPlus.value) {
|
if (!showAd.value) {
|
||||||
hide_ads_window(true)
|
hide_ads_window(true)
|
||||||
} else {
|
} else {
|
||||||
show_ads_window()
|
setTimeout(() => {
|
||||||
|
init_ads_window(true)
|
||||||
|
}, 400)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@ -367,21 +370,21 @@ function handleAuxClick(e) {
|
|||||||
<InstanceCreationModal ref="installationModal" />
|
<InstanceCreationModal ref="installationModal" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<div
|
<div
|
||||||
class="app-grid-navbar bg-bg-raised flex flex-col p-[1rem] pt-0 gap-[0.5rem] z-10 w-[--left-bar-width]"
|
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
|
||||||
>
|
>
|
||||||
<NavButton to="/">
|
<NavButton v-tooltip.right="'Home'" to="/">
|
||||||
<HomeIcon />
|
<HomeIcon />
|
||||||
<template #label>Home</template>
|
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<NavButton
|
<NavButton
|
||||||
|
v-tooltip.right="'Discover content'"
|
||||||
to="/browse/modpack"
|
to="/browse/modpack"
|
||||||
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
|
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
|
||||||
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
|
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
|
||||||
>
|
>
|
||||||
<CompassIcon />
|
<CompassIcon />
|
||||||
<template #label>Discover content</template>
|
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<NavButton
|
<NavButton
|
||||||
|
v-tooltip.right="'Library'"
|
||||||
to="/library"
|
to="/library"
|
||||||
:is-subpage="
|
:is-subpage="
|
||||||
() =>
|
() =>
|
||||||
@ -391,24 +394,24 @@ function handleAuxClick(e) {
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<LibraryIcon />
|
<LibraryIcon />
|
||||||
<template #label>Library</template>
|
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||||
<suspense>
|
<suspense>
|
||||||
<QuickInstanceSwitcher />
|
<QuickInstanceSwitcher />
|
||||||
</suspense>
|
</suspense>
|
||||||
<NavButton :to="() => $refs.installationModal.show()" :disabled="offline">
|
<NavButton
|
||||||
|
v-tooltip.right="'Create new instance'"
|
||||||
|
:to="() => $refs.installationModal.show()"
|
||||||
|
:disabled="offline"
|
||||||
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
<template #label>Create new instance</template>
|
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<div class="flex flex-grow"></div>
|
<div class="flex flex-grow"></div>
|
||||||
<NavButton v-if="updateAvailable" :to="() => restartApp()">
|
<NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
<template #label>Install update</template>
|
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<NavButton :to="() => $refs.settingsModal.show()">
|
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
<template #label>Settings</template>
|
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<ButtonStyled v-if="credentials" type="transparent" circular>
|
<ButtonStyled v-if="credentials" type="transparent" circular>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
@ -430,17 +433,30 @@ function handleAuxClick(e) {
|
|||||||
<template #sign-out> <LogOutIcon /> Sign out </template>
|
<template #sign-out> <LogOutIcon /> Sign out </template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<NavButton v-else :to="() => signIn()">
|
<NavButton v-else v-tooltip.right="'Sign in'" :to="() => signIn()">
|
||||||
<LogInIcon />
|
<LogInIcon />
|
||||||
<template #label>Sign in</template>
|
<template #label>Sign in</template>
|
||||||
</NavButton>
|
</NavButton>
|
||||||
</div>
|
</div>
|
||||||
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
|
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
|
||||||
<div data-tauri-drag-region class="flex p-4">
|
<div data-tauri-drag-region class="flex p-3">
|
||||||
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
|
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
|
||||||
<Breadcrumbs />
|
<Breadcrumbs />
|
||||||
</div>
|
</div>
|
||||||
<section class="flex ml-auto">
|
<section class="flex ml-auto items-center">
|
||||||
|
<ButtonStyled
|
||||||
|
v-if="!forceSidebar && themeStore.toggleSidebar"
|
||||||
|
:type="sidebarToggled ? 'standard' : 'transparent'"
|
||||||
|
circular
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="mr-3 transition-transform"
|
||||||
|
:class="{ 'rotate-180': !sidebarToggled }"
|
||||||
|
@click="sidebarToggled = !sidebarToggled"
|
||||||
|
>
|
||||||
|
<RightArrowIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
<div class="flex mr-3">
|
<div class="flex mr-3">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<RunningAppBar />
|
<RunningAppBar />
|
||||||
@ -465,7 +481,11 @@ function handleAuxClick(e) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="stateInitialized" class="app-contents experimental-styles-within">
|
<div
|
||||||
|
v-if="stateInitialized"
|
||||||
|
class="app-contents experimental-styles-within"
|
||||||
|
:class="{ 'sidebar-enabled': sidebarVisible }"
|
||||||
|
>
|
||||||
<div class="app-viewport flex-grow router-view">
|
<div class="app-viewport flex-grow router-view">
|
||||||
<div
|
<div
|
||||||
class="loading-indicator-container h-8 fixed z-50"
|
class="loading-indicator-container h-8 fixed z-50"
|
||||||
@ -478,7 +498,7 @@ function handleAuxClick(e) {
|
|||||||
<ModrinthLoadingIndicator />
|
<ModrinthLoadingIndicator />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="themeStore.featureFlag_pagePath"
|
v-if="themeStore.featureFlags.page_path"
|
||||||
class="absolute bottom-0 left-0 m-2 bg-tooltip-bg text-tooltip-text font-semibold rounded-full px-2 py-1 text-xs z-50"
|
class="absolute bottom-0 left-0 m-2 bg-tooltip-bg text-tooltip-text font-semibold rounded-full px-2 py-1 text-xs z-50"
|
||||||
>
|
>
|
||||||
{{ route.fullPath }}
|
{{ route.fullPath }}
|
||||||
@ -507,7 +527,7 @@ function handleAuxClick(e) {
|
|||||||
:class="{ 'pb-12': !hasPlus }"
|
:class="{ 'pb-12': !hasPlus }"
|
||||||
>
|
>
|
||||||
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
|
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
|
||||||
<div class="sidebar-default-content">
|
<div class="sidebar-default-content" :class="{ 'sidebar-enabled': sidebarVisible }">
|
||||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||||
<h3 class="text-lg m-0">Playing as</h3>
|
<h3 class="text-lg m-0">Playing as</h3>
|
||||||
<suspense>
|
<suspense>
|
||||||
@ -519,7 +539,7 @@ function handleAuxClick(e) {
|
|||||||
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||||
</suspense>
|
</suspense>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-4 flex flex-col">
|
<div v-if="news && news.length > 0" class="pt-4 flex flex-col">
|
||||||
<h3 class="px-4 text-lg m-0">News</h3>
|
<h3 class="px-4 text-lg m-0">News</h3>
|
||||||
<template v-for="(item, index) in news" :key="`news-${index}`">
|
<template v-for="(item, index) in news" :key="`news-${index}`">
|
||||||
<a
|
<a
|
||||||
@ -550,7 +570,7 @@ function handleAuxClick(e) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!hasPlus">
|
<template v-if="showAd">
|
||||||
<a
|
<a
|
||||||
href="https://modrinth.plus?app"
|
href="https://modrinth.plus?app"
|
||||||
class="absolute bottom-[250px] w-full flex justify-center items-center gap-1 px-4 py-3 text-purple font-medium hover:underline z-10"
|
class="absolute bottom-[250px] w-full flex justify-center items-center gap-1 px-4 py-3 text-purple font-medium hover:underline z-10"
|
||||||
@ -577,21 +597,6 @@ function handleAuxClick(e) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.sleek-primary {
|
|
||||||
background-color: var(--color-brand-highlight);
|
|
||||||
transition: all ease-in-out 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation-controls {
|
|
||||||
flex-grow: 1;
|
|
||||||
width: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appbar-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-controls {
|
.window-controls {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: none;
|
display: none;
|
||||||
@ -658,139 +663,10 @@ function handleAuxClick(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-container {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.view {
|
|
||||||
width: calc(100% - var(--sidebar-width));
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
|
|
||||||
.critical-error-banner {
|
|
||||||
margin-top: -1.25rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: rgba(203, 34, 69, 0.1);
|
|
||||||
border-left: 2px solid var(--color-red);
|
|
||||||
border-bottom: 2px solid var(--color-red);
|
|
||||||
border-right: 2px solid var(--color-red);
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
background: var(--color-raised-bg);
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--gap-md);
|
|
||||||
height: 3.25rem;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
//no select
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.router-view {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - 3.125rem);
|
|
||||||
overflow: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border-top-left-radius: var(--radius-xl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
|
|
||||||
padding: var(--gap-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pages-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
word-spacing: 3px;
|
|
||||||
background: inherit;
|
|
||||||
transition: all ease-in-out 0.1s;
|
|
||||||
color: var(--color-base);
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
&.router-link-active {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
background: var(--color-button-bg);
|
|
||||||
box-shadow: var(--shadow-floating);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-button-bg);
|
|
||||||
color: var(--color-contrast);
|
|
||||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
color: var(--color-accent-contrast);
|
|
||||||
background-color: var(--color-brand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsed-button {
|
|
||||||
height: 3rem !important;
|
|
||||||
width: 3rem !important;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1.5rem !important;
|
|
||||||
height: 1.5rem !important;
|
|
||||||
max-width: 1.5rem !important;
|
|
||||||
max-height: 1.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
padding: var(--gap-sm) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-grid-layout,
|
.app-grid-layout,
|
||||||
.app-contents {
|
.app-contents {
|
||||||
--top-bar-height: 3.75rem;
|
--top-bar-height: 3rem;
|
||||||
--left-bar-width: 5rem;
|
--left-bar-width: 4rem;
|
||||||
--right-bar-width: 300px;
|
--right-bar-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -816,18 +692,21 @@ function handleAuxClick(e) {
|
|||||||
.app-contents {
|
.app-contents {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
left: 5rem;
|
left: var(--left-bar-width);
|
||||||
top: 3.75rem;
|
top: var(--top-bar-height);
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: calc(100vh - 3.75rem);
|
height: calc(100vh - var(--top-bar-height));
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
border-top-left-radius: var(--radius-xl);
|
border-top-left-radius: var(--radius-xl);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 300px;
|
grid-template-columns: 1fr 0px;
|
||||||
//grid-template-columns: 1fr 0px;
|
|
||||||
transition: grid-template-columns 0.4s ease-in-out;
|
transition: grid-template-columns 0.4s ease-in-out;
|
||||||
|
|
||||||
|
&.sidebar-enabled {
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-indicator-container {
|
.loading-indicator-container {
|
||||||
@ -839,7 +718,7 @@ function handleAuxClick(e) {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: calc(100vh - 3.75rem);
|
height: calc(100vh - var(--top-bar-height));
|
||||||
background: var(--brand-gradient-bg);
|
background: var(--brand-gradient-bg);
|
||||||
|
|
||||||
--color-button-bg: var(--brand-gradient-button);
|
--color-button-bg: var(--brand-gradient-button);
|
||||||
@ -881,22 +760,15 @@ function handleAuxClick(e) {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
//::-webkit-scrollbar-track {
|
|
||||||
// background-color: transparent; /* Make it transparent if needed */
|
|
||||||
// margin-block: 5px;
|
|
||||||
// margin-right: 5px;
|
|
||||||
//}
|
|
||||||
|
|
||||||
.app-contents::before {
|
.app-contents::before {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 5rem;
|
left: var(--left-bar-width);
|
||||||
top: 3.75rem;
|
top: var(--top-bar-height);
|
||||||
right: -5rem;
|
right: calc(-1 * var(--left-bar-width));
|
||||||
bottom: -5rem;
|
bottom: calc(-1 * var(--left-bar-width));
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
//box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.2) inset;
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
|
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
|
||||||
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
|
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
|
||||||
@ -911,7 +783,7 @@ function handleAuxClick(e) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-teleport-content:empty + .sidebar-default-content {
|
.sidebar-teleport-content:empty + .sidebar-default-content.sidebar-enabled {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -218,14 +218,14 @@ const filteredResults = computed(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="iconified-input">
|
|
||||||
<SearchIcon />
|
|
||||||
<input v-model="search" type="text" class="h-12" placeholder="Search" />
|
|
||||||
<Button class="r-btn" @click="() => (search = '')">
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<div class="iconified-input flex-1">
|
||||||
|
<SearchIcon />
|
||||||
|
<input v-model="search" type="text" placeholder="Search" />
|
||||||
|
<Button class="r-btn" @click="() => (search = '')">
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
v-slot="{ selected }"
|
v-slot="{ selected }"
|
||||||
v-model="sortBy"
|
v-model="sortBy"
|
||||||
@ -363,7 +363,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
.instances {
|
.instances {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|||||||
@ -207,13 +207,18 @@ const calculateCardsPerRow = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rowContainer = ref(null)
|
||||||
|
const resizeObserver = ref(null)
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calculateCardsPerRow()
|
calculateCardsPerRow()
|
||||||
|
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
||||||
|
resizeObserver.value.observe(rowContainer.value)
|
||||||
window.addEventListener('resize', calculateCardsPerRow)
|
window.addEventListener('resize', calculateCardsPerRow)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', calculateCardsPerRow)
|
window.removeEventListener('resize', calculateCardsPerRow)
|
||||||
|
resizeObserver.value.unobserve(rowContainer.value)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -226,7 +231,7 @@ onUnmounted(() => {
|
|||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
@proceed="deleteProfile"
|
@proceed="deleteProfile"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-4">
|
<div ref="rowContainer" class="flex flex-col gap-4">
|
||||||
<div v-for="(row, rowIndex) in actualInstances" ref="rows" :key="row.label" class="row">
|
<div v-for="(row, rowIndex) in actualInstances" ref="rows" :key="row.label" class="row">
|
||||||
<router-link
|
<router-link
|
||||||
class="flex mb-3 leading-none items-center gap-1 text-primary text-lg font-bold hover:underline group"
|
class="flex mb-3 leading-none items-center gap-1 text-primary text-lg font-bold hover:underline group"
|
||||||
|
|||||||
@ -12,11 +12,9 @@ import { showProfileInFolder } from '@/helpers/utils.js'
|
|||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import duration from 'dayjs/plugin/duration'
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { formatCategory } from '@modrinth/utils'
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
|
||||||
dayjs.extend(duration)
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -169,7 +167,7 @@ onUnmounted(() => unlisten())
|
|||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center">
|
<div class="relative flex items-center justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="96px"
|
size="48px"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
alt="Mod card"
|
alt="Mod card"
|
||||||
@ -205,7 +203,7 @@ onUnmounted(() => unlisten())
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<p class="m-0 text-lg font-bold text-contrast leading-tight line-clamp-2">
|
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||||
@ -214,17 +212,6 @@ onUnmounted(() => unlisten())
|
|||||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
|
||||||
<TimerIcon class="shrink-0" />
|
|
||||||
<span class="text-sm line-clamp-1">
|
|
||||||
Played for
|
|
||||||
{{
|
|
||||||
dayjs
|
|
||||||
.duration(instance.recent_time_played + instance.submitted_time_played, 'seconds')
|
|
||||||
.humanize()
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,29 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tooltip-parent flex items-center justify-center">
|
<RouterLink
|
||||||
<RouterLink
|
v-if="typeof to === 'string'"
|
||||||
v-if="typeof to === 'string'"
|
:to="to"
|
||||||
:to="to"
|
v-bind="$attrs"
|
||||||
v-bind="$attrs"
|
:class="{
|
||||||
:class="{
|
'router-link-active': isPrimary && isPrimary(route),
|
||||||
'router-link-active': isPrimary && isPrimary(route),
|
'subpage-active': isSubpage && isSubpage(route),
|
||||||
'subpage-active': isSubpage && isSubpage(route),
|
}"
|
||||||
}"
|
class="w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
class="w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
>
|
||||||
>
|
<slot />
|
||||||
<slot />
|
</RouterLink>
|
||||||
</RouterLink>
|
<button
|
||||||
<button
|
v-else
|
||||||
v-else
|
v-bind="$attrs"
|
||||||
v-bind="$attrs"
|
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
@click="to"
|
||||||
@click="to"
|
>
|
||||||
>
|
<slot />
|
||||||
<slot />
|
</button>
|
||||||
</button>
|
|
||||||
<div class="tooltip-label">
|
|
||||||
<slot name="label" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -61,50 +56,4 @@ defineOptions({
|
|||||||
.subpage-active {
|
.subpage-active {
|
||||||
@apply text-contrast bg-button-bg;
|
@apply text-contrast bg-button-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-parent {
|
|
||||||
position: relative;
|
|
||||||
border-radius: var(--radius-max);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-parent:hover .tooltip-label {
|
|
||||||
opacity: 1;
|
|
||||||
translate: 0 0;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-label:not(:empty) {
|
|
||||||
--_tooltip-bg: black;
|
|
||||||
--_tooltip-color: var(--dark-color-contrast);
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
background-color: var(--_tooltip-bg);
|
|
||||||
color: var(--_tooltip-color);
|
|
||||||
text-wrap: nowrap;
|
|
||||||
padding: 0.5rem 0.5rem;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
left: calc(100% + 0.5rem);
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: bold;
|
|
||||||
filter: drop-shadow(5px 5px 0.8rem rgba(0, 0, 0, 0.35));
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
translate: -0.5rem 0;
|
|
||||||
scale: 0.9;
|
|
||||||
transition: all ease-in-out 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-label:not(:empty)::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: 100%; /* To the left of the tooltip */
|
|
||||||
margin-top: -5px;
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent var(--_tooltip-bg) transparent transparent;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
function updateAdPosition() {
|
function updateAdPosition() {
|
||||||
if (adsWrapper.value) {
|
if (adsWrapper.value) {
|
||||||
init_ads_window(true)
|
init_ads_window()
|
||||||
initDevicePixelRatioWatcher()
|
initDevicePixelRatioWatcher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,14 @@ const getInstances = async () => {
|
|||||||
|
|
||||||
recentInstances.value = profiles
|
recentInstances.value = profiles
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const dateA = dayjs(a.created > a.last_played ? a.last_played : a.created)
|
const dateACreated = dayjs(a.created)
|
||||||
const dateB = dayjs(b.created > b.last_played ? b.last_played : b.created)
|
const dateAPlayed = dayjs(a.last_played)
|
||||||
|
|
||||||
|
const dateBCreated = dayjs(b.created)
|
||||||
|
const dateBPlayed = dayjs(b.last_played)
|
||||||
|
|
||||||
|
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
||||||
|
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
||||||
|
|
||||||
if (dateA.isSame(dateB)) {
|
if (dateA.isSame(dateB)) {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
@ -42,6 +48,7 @@ onUnmounted(() => {
|
|||||||
<NavButton
|
<NavButton
|
||||||
v-for="instance in recentInstances"
|
v-for="instance in recentInstances"
|
||||||
:key="instance.id"
|
:key="instance.id"
|
||||||
|
v-tooltip.right="instance.name"
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -56,8 +63,8 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<template #label>{{ instance.name }}</template>
|
|
||||||
</NavButton>
|
</NavButton>
|
||||||
|
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="action-groups">
|
<div class="action-groups">
|
||||||
<a href="https://support.modrinth.com" class="link">
|
|
||||||
<ChatIcon />
|
|
||||||
<span> Get support </span>
|
|
||||||
</a>
|
|
||||||
<Button
|
<Button
|
||||||
v-if="currentLoadingBars.length > 0"
|
v-if="currentLoadingBars.length > 0"
|
||||||
ref="infoButton"
|
ref="infoButton"
|
||||||
@ -123,7 +119,6 @@ import { useRouter } from 'vue-router'
|
|||||||
import { progress_bars_list } from '@/helpers/state.js'
|
import { progress_bars_list } from '@/helpers/state.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 { ChatIcon } from '@/assets/icons'
|
|
||||||
import { get_many } from '@/helpers/profile.js'
|
import { get_many } from '@/helpers/profile.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
<div class="m-0 line-clamp-2">
|
<div class="m-0 line-clamp-2">
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-auto flex items-center gap-1 no-wrap">
|
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
||||||
<TagsIcon class="h-4 w-4 shrink-0" />
|
<TagsIcon class="h-4 w-4 shrink-0" />
|
||||||
<div
|
<div
|
||||||
v-for="tag in categories"
|
v-for="tag in categories"
|
||||||
|
|||||||
@ -190,6 +190,7 @@ const messages = defineMessages({
|
|||||||
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
|
:show-ad-on-close="false"
|
||||||
@proceed="removeProfile"
|
@proceed="removeProfile"
|
||||||
/>
|
/>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
|||||||
@ -457,6 +457,7 @@ const messages = defineMessages({
|
|||||||
:proceed-icon="HammerIcon"
|
:proceed-icon="HammerIcon"
|
||||||
:proceed-label="formatMessage(messages.repairButton)"
|
:proceed-label="formatMessage(messages.repairButton)"
|
||||||
:danger="false"
|
:danger="false"
|
||||||
|
:show-ad-on-close="false"
|
||||||
@proceed="() => repairProfile(true)"
|
@proceed="() => repairProfile(true)"
|
||||||
/>
|
/>
|
||||||
<ModpackVersionModal
|
<ModpackVersionModal
|
||||||
@ -480,6 +481,7 @@ const messages = defineMessages({
|
|||||||
:description="formatMessage(messages.unlinkInstanceConfirmDescription)"
|
:description="formatMessage(messages.unlinkInstanceConfirmDescription)"
|
||||||
:proceed-icon="UnlinkIcon"
|
:proceed-icon="UnlinkIcon"
|
||||||
:proceed-label="formatMessage(messages.unlinkInstanceButton)"
|
:proceed-label="formatMessage(messages.unlinkInstanceButton)"
|
||||||
|
:show-ad-on-close="false"
|
||||||
@proceed="() => unpairProfile()"
|
@proceed="() => unpairProfile()"
|
||||||
/>
|
/>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
@ -488,6 +490,7 @@ const messages = defineMessages({
|
|||||||
:description="formatMessage(messages.reinstallModpackConfirmDescription)"
|
:description="formatMessage(messages.reinstallModpackConfirmDescription)"
|
||||||
:proceed-icon="DownloadIcon"
|
:proceed-icon="DownloadIcon"
|
||||||
:proceed-label="formatMessage(messages.reinstallModpackButton)"
|
:proceed-label="formatMessage(messages.reinstallModpackButton)"
|
||||||
|
:show-ad-on-close="false"
|
||||||
@proceed="() => repairModpack()"
|
@proceed="() => repairModpack()"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
CoffeeIcon,
|
CoffeeIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { TabbedModal } from '@modrinth/ui'
|
import { TabbedModal } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||||
@ -22,6 +22,7 @@ import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/
|
|||||||
import { useTheming } from '@/store/state'
|
import { useTheming } from '@/store/state'
|
||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { get, set } from '@/helpers/settings'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@ -99,6 +100,28 @@ defineExpose({ show, isOpen })
|
|||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
const osPlatform = getOsPlatform()
|
const osPlatform = getOsPlatform()
|
||||||
const osVersion = getOsVersion()
|
const osVersion = getOsVersion()
|
||||||
|
const settings = ref(await get())
|
||||||
|
|
||||||
|
watch(
|
||||||
|
settings,
|
||||||
|
async () => {
|
||||||
|
await set(settings.value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
function devModeCount() {
|
||||||
|
devModeCounter.value++
|
||||||
|
if (devModeCounter.value > 5) {
|
||||||
|
themeStore.devMode = !themeStore.devMode
|
||||||
|
settings.value.developer_mode = !!themeStore.devMode
|
||||||
|
devModeCounter.value = 0
|
||||||
|
|
||||||
|
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
|
||||||
|
modal.value.setTab(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
@ -118,19 +141,7 @@ const osVersion = getOsVersion()
|
|||||||
<button
|
<button
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
||||||
@click="
|
@click="devModeCount"
|
||||||
() => {
|
|
||||||
devModeCounter++
|
|
||||||
if (devModeCounter > 5) {
|
|
||||||
themeStore.devMode = !themeStore.devMode
|
|
||||||
devModeCounter = 0
|
|
||||||
|
|
||||||
if (!themeStore.devMode && tabs[modal.selectedTab].developerOnly) {
|
|
||||||
modal.setTab(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<ModrinthIcon class="w-6 h-6" />
|
<ModrinthIcon class="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { useTheming } from '@/store/theme.js'
|
|||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
confirmationText: {
|
confirmationText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@ -37,6 +37,10 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showAdOnClose: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['proceed'])
|
const emit = defineEmits(['proceed'])
|
||||||
@ -54,7 +58,9 @@ defineExpose({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
show_ads_window()
|
if (props.showAdOnClose) {
|
||||||
|
show_ads_window()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function proceed() {
|
function proceed() {
|
||||||
|
|||||||
@ -23,8 +23,13 @@ watch(
|
|||||||
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
||||||
|
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
:update-color-theme="themeStore.setThemeState"
|
:update-color-theme="
|
||||||
:current-theme="themeStore.selectedTheme"
|
(theme) => {
|
||||||
|
themeStore.setThemeState(theme)
|
||||||
|
settings.theme = theme
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:current-theme="settings.theme"
|
||||||
:theme-options="themeStore.themeOptions"
|
:theme-options="themeStore.themeOptions"
|
||||||
system-theme-color="system"
|
system-theme-color="system"
|
||||||
/>
|
/>
|
||||||
@ -97,4 +102,22 @@ watch(
|
|||||||
:options="['Home', 'Library']"
|
:options="['Home', 'Library']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||||
|
<p class="m-0 mt-1">Enables the ability to toggle the sidebar.</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
id="toggle-sidebar"
|
||||||
|
:model-value="settings.toggle_sidebar"
|
||||||
|
:checked="settings.toggle_sidebar"
|
||||||
|
@update:model-value="
|
||||||
|
(e) => {
|
||||||
|
settings.toggle_sidebar = e
|
||||||
|
themeStore.toggleSidebar = settings.toggle_sidebar
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,34 +1,36 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
import { useTheming } from '@/store/state'
|
||||||
import { computed } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import { get, set } from '@/helpers/settings'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
type ThemeStoreKeys = keyof typeof themeStore
|
const settings = ref(await get())
|
||||||
|
const options = ref(['project_background', 'page_path'])
|
||||||
|
|
||||||
const options: Ref<ThemeStoreKeys[]> = computed(() => {
|
function getStoreValue(key: string) {
|
||||||
return Object.keys(themeStore).filter((key) => key.startsWith('featureFlag_')) as ThemeStoreKeys[]
|
return themeStore.featureFlags[key] ?? false
|
||||||
})
|
|
||||||
|
|
||||||
function getStoreValue<K extends ThemeStoreKeys>(key: K): (typeof themeStore)[K] {
|
|
||||||
return themeStore[key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStoreValue<K extends ThemeStoreKeys>(key: K, value: (typeof themeStore)[K]) {
|
function setStoreValue(key: string, value: boolean) {
|
||||||
themeStore[key] = value
|
themeStore.featureFlags[key] = value
|
||||||
|
settings.value.feature_flags[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFlagName(name: string) {
|
watch(
|
||||||
return name.replace('featureFlag_', '')
|
settings,
|
||||||
}
|
async () => {
|
||||||
|
await set(settings.value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
||||||
{{ formatFlagName(option) }}
|
{{ option }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ function formatFlagName(name: string) {
|
|||||||
id="advanced-rendering"
|
id="advanced-rendering"
|
||||||
:model-value="getStoreValue(option)"
|
:model-value="getStoreValue(option)"
|
||||||
:checked="getStoreValue(option)"
|
:checked="getStoreValue(option)"
|
||||||
@update:model-value="() => setStoreValue(option, !themeStore[option])"
|
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -80,6 +80,7 @@ async function findLauncherDir() {
|
|||||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Purge cache"
|
proceed-label="Purge cache"
|
||||||
|
:show-ad-on-close="false"
|
||||||
@proceed="purgeCache"
|
@proceed="purgeCache"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -169,10 +169,14 @@ window.addEventListener('online', () => {
|
|||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
breadcrumbs.setContext({ name: 'Discover content', link: route.path, query: route.query })
|
breadcrumbs.setContext({ name: 'Discover content', link: route.path, query: route.query })
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(true)
|
||||||
|
|
||||||
const projectType = ref(route.params.projectType)
|
const projectType = ref(route.params.projectType)
|
||||||
|
|
||||||
|
watch(projectType, () => {
|
||||||
|
loading.value = true
|
||||||
|
})
|
||||||
|
|
||||||
type SearchResult = {
|
type SearchResult = {
|
||||||
project_id: string
|
project_id: string
|
||||||
}
|
}
|
||||||
@ -240,6 +244,7 @@ async function refreshSearch() {
|
|||||||
query: params,
|
query: params,
|
||||||
})
|
})
|
||||||
await router.replace({ path: route.path, query: params })
|
await router.replace({ path: route.path, query: params })
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setPage(newPageNumber: number) {
|
async function setPage(newPageNumber: number) {
|
||||||
@ -375,10 +380,12 @@ await refreshSearch()
|
|||||||
button-class="button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg"
|
button-class="button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg"
|
||||||
content-class="mb-3"
|
content-class="mb-3"
|
||||||
inner-panel-class="ml-2 mr-3"
|
inner-panel-class="ml-2 mr-3"
|
||||||
:open-by-default="filter.id.startsWith('category') || filter.id === 'environment'"
|
:open-by-default="
|
||||||
|
filter.id.startsWith('category') || filter.id === 'environment' || filter.id === 'license'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h3 class="text-lg m-0">{{ filter.formatted_name }}</h3>
|
<h3 class="text-base m-0">{{ filter.formatted_name }}</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #locked-game_version>
|
<template #locked-game_version>
|
||||||
{{ formatMessage(messages.gameVersionProvidedByInstance) }}
|
{{ formatMessage(messages.gameVersionProvidedByInstance) }}
|
||||||
@ -394,7 +401,6 @@ await refreshSearch()
|
|||||||
<InstanceIndicator :instance="instance" />
|
<InstanceIndicator :instance="instance" />
|
||||||
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
|
||||||
</template>
|
</template>
|
||||||
<h1 v-else class="m-0 text-2xl">Discover content</h1>
|
|
||||||
<NavTabs :links="selectableProjectTypes" />
|
<NavTabs :links="selectableProjectTypes" />
|
||||||
<div class="iconified-input">
|
<div class="iconified-input">
|
||||||
<SearchIcon aria-hidden="true" class="text-lg" />
|
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||||
@ -465,11 +471,10 @@ await refreshSearch()
|
|||||||
loader.supported_project_types?.includes(projectType),
|
loader.supported_project_types?.includes(projectType),
|
||||||
),
|
),
|
||||||
]"
|
]"
|
||||||
:installed="result.installed"
|
:installed="result.installed || newlyInstalled.includes(result.project_id)"
|
||||||
@install="
|
@install="
|
||||||
(id) => {
|
(id) => {
|
||||||
newlyInstalled.push(id)
|
newlyInstalled.push(id)
|
||||||
refreshSearch()
|
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -16,17 +16,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<AddContentButton :instance="instance" />
|
<AddContentButton :instance="instance" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="filterOptions.length > 1" class="flex flex-wrap gap-1 items-center pb-4">
|
<div class="flex items-center justify-between">
|
||||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
<div v-if="filterOptions.length > 1" class="flex flex-wrap gap-1 items-center pb-4">
|
||||||
<button
|
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||||
v-for="filter in filterOptions"
|
<button
|
||||||
:key="filter"
|
v-for="filter in filterOptions"
|
||||||
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
:key="filter"
|
||||||
@click="toggleArray(selectedFilters, filter.id)"
|
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
||||||
>
|
@click="toggleArray(selectedFilters, filter.id)"
|
||||||
{{ filter.formattedName }}
|
>
|
||||||
</button>
|
{{ filter.formattedName }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
v-if="search.length > 0"
|
||||||
|
:page="currentPage"
|
||||||
|
:count="Math.ceil(search.length / 20)"
|
||||||
|
:link-function="(page) => `?page=${page}`"
|
||||||
|
@switch-page="(page) => (currentPage = page)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ContentListPanel
|
<ContentListPanel
|
||||||
v-model="selectedFiles"
|
v-model="selectedFiles"
|
||||||
:locked="isPackLocked"
|
:locked="isPackLocked"
|
||||||
@ -70,6 +80,7 @@
|
|||||||
:sort-column="sortColumn"
|
:sort-column="sortColumn"
|
||||||
:sort-ascending="ascending"
|
:sort-ascending="ascending"
|
||||||
:update-sort="sortProjects"
|
:update-sort="sortProjects"
|
||||||
|
:current-page="currentPage"
|
||||||
>
|
>
|
||||||
<template v-if="selectedProjects.length > 0" #headers>
|
<template v-if="selectedProjects.length > 0" #headers>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@ -238,28 +249,28 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ExternalIcon,
|
CheckCircleIcon,
|
||||||
LinkIcon,
|
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
TrashIcon,
|
|
||||||
SearchIcon,
|
|
||||||
UpdatedIcon,
|
|
||||||
XIcon,
|
|
||||||
ShareIcon,
|
|
||||||
DropdownIcon,
|
|
||||||
FileIcon,
|
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
DropdownIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
FileIcon,
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
|
LinkIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
CheckCircleIcon,
|
SearchIcon,
|
||||||
|
ShareIcon,
|
||||||
SlashIcon,
|
SlashIcon,
|
||||||
|
TrashIcon,
|
||||||
|
UpdatedIcon,
|
||||||
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, ButtonStyled, ContentListPanel, OverflowMenu } from '@modrinth/ui'
|
import { Button, ButtonStyled, ContentListPanel, OverflowMenu, Pagination } from '@modrinth/ui'
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
import { formatProjectType } from '@modrinth/utils'
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef } from 'vue'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useVIntl, defineMessages } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import {
|
import {
|
||||||
add_project_from_path,
|
add_project_from_path,
|
||||||
get_projects,
|
get_projects,
|
||||||
@ -405,7 +416,7 @@ const initProjects = async (cacheBehaviour?) => {
|
|||||||
icon: null,
|
icon: null,
|
||||||
disabled: file.file_name.endsWith('.disabled'),
|
disabled: file.file_name.endsWith('.disabled'),
|
||||||
outdated: false,
|
outdated: false,
|
||||||
project_type: file.project_type,
|
project_type: file.project_type === 'shaderpack' ? 'shader' : file.project_type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,6 +495,15 @@ const filteredProjects = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(filterOptions, () => {
|
||||||
|
for (let i = 0; i < selectedFilters.value.length; i++) {
|
||||||
|
const option = selectedFilters.value[i]
|
||||||
|
if (!filterOptions.value.some((x) => x.id === option)) {
|
||||||
|
selectedFilters.value.splice(i, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function toggleArray(array, value) {
|
function toggleArray(array, value) {
|
||||||
if (array.includes(value)) {
|
if (array.includes(value)) {
|
||||||
array.splice(array.indexOf(value), 1)
|
array.splice(array.indexOf(value), 1)
|
||||||
@ -494,11 +514,10 @@ function toggleArray(array, value) {
|
|||||||
|
|
||||||
const searchFilter = ref('')
|
const searchFilter = ref('')
|
||||||
const selectAll = ref(false)
|
const selectAll = ref(false)
|
||||||
const selectedProjectType = ref('All')
|
|
||||||
const hideNonSelected = ref(false)
|
|
||||||
const shareModal = ref(null)
|
const shareModal = ref(null)
|
||||||
const ascending = ref(true)
|
const ascending = ref(true)
|
||||||
const sortColumn = ref('Name')
|
const sortColumn = ref('Name')
|
||||||
|
const currentPage = ref(1)
|
||||||
|
|
||||||
const selected = computed(() =>
|
const selected = computed(() =>
|
||||||
Array.from(selectionMap.value)
|
Array.from(selectionMap.value)
|
||||||
@ -514,32 +533,10 @@ const functionValues = computed(() =>
|
|||||||
selectedProjects.value.length > 0 ? selectedProjects.value : Array.from(projects.value.values()),
|
selectedProjects.value.length > 0 ? selectedProjects.value : Array.from(projects.value.values()),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectableProjectTypes = computed(() => {
|
|
||||||
const obj = { All: 'all' }
|
|
||||||
|
|
||||||
for (const project of projects.value) {
|
|
||||||
obj[project.project_type ? formatProjectType(project.project_type) + 's' : 'Other'] =
|
|
||||||
project.project_type
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
|
|
||||||
const search = computed(() => {
|
const search = computed(() => {
|
||||||
const projectType = selectableProjectTypes.value[selectedProjectType.value]
|
const filtered = filteredProjects.value.filter((mod) => {
|
||||||
const filtered = filteredProjects.value
|
return mod.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||||
.filter((mod) => {
|
})
|
||||||
return (
|
|
||||||
mod.name.toLowerCase().includes(searchFilter.value.toLowerCase()) &&
|
|
||||||
(projectType === 'all' || mod.project_type === projectType)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.filter((mod) => {
|
|
||||||
if (hideNonSelected.value) {
|
|
||||||
return !mod.disabled
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
switch (sortColumn.value) {
|
switch (sortColumn.value) {
|
||||||
case 'Updated':
|
case 'Updated':
|
||||||
@ -553,18 +550,12 @@ const search = computed(() => {
|
|||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
return filtered.slice().sort((a, b) => {
|
return filtered.slice().sort((a, b) => a.name.localeCompare(b.name))
|
||||||
if (a.name < b.name) {
|
|
||||||
return ascending.value ? -1 : 1
|
|
||||||
}
|
|
||||||
if (a.name > b.name) {
|
|
||||||
return ascending.value ? 1 : -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(search, () => (currentPage.value = 1))
|
||||||
|
|
||||||
const sortProjects = (filter) => {
|
const sortProjects = (filter) => {
|
||||||
if (sortColumn.value === filter) {
|
if (sortColumn.value === filter) {
|
||||||
ascending.value = !ascending.value
|
ascending.value = !ascending.value
|
||||||
@ -630,35 +621,33 @@ const locks = {}
|
|||||||
|
|
||||||
const toggleDisableMod = async (mod) => {
|
const toggleDisableMod = async (mod) => {
|
||||||
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
|
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
|
||||||
if (!locks[mod.id]) {
|
const lock = locks[mod.file_name]
|
||||||
locks[mod.id] = ref(null)
|
|
||||||
|
while (lock) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout((_) => resolve(), 100)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const lock = locks[mod.id]
|
locks[mod.file_name] = 'lock'
|
||||||
|
|
||||||
while (lock.value) {
|
try {
|
||||||
await lock.value
|
mod.path = await toggle_disable_project(props.instance.path, mod.path)
|
||||||
|
mod.disabled = !mod.disabled
|
||||||
|
|
||||||
|
trackEvent('InstanceProjectDisable', {
|
||||||
|
loader: props.instance.loader,
|
||||||
|
game_version: props.instance.game_version,
|
||||||
|
id: mod.id,
|
||||||
|
name: mod.name,
|
||||||
|
project_type: mod.project_type,
|
||||||
|
disabled: mod.disabled,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
lock.value = toggle_disable_project(props.instance.path, mod.path)
|
locks[mod.file_name] = null
|
||||||
.then((newPath) => {
|
|
||||||
mod.path = newPath
|
|
||||||
mod.disabled = !mod.disabled
|
|
||||||
trackEvent('InstanceProjectDisable', {
|
|
||||||
loader: props.instance.loader,
|
|
||||||
game_version: props.instance.game_version,
|
|
||||||
id: mod.id,
|
|
||||||
name: mod.name,
|
|
||||||
project_type: mod.project_type,
|
|
||||||
disabled: mod.disabled,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => {
|
|
||||||
lock.value = null
|
|
||||||
})
|
|
||||||
|
|
||||||
await lock.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeMod = async (mod) => {
|
const removeMod = async (mod) => {
|
||||||
|
|||||||
@ -1,973 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ConfirmModalWrapper
|
|
||||||
ref="modal_confirm"
|
|
||||||
title="Are you sure you want to delete this instance?"
|
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
|
||||||
:has-to-type="false"
|
|
||||||
proceed-label="Delete"
|
|
||||||
@proceed="removeProfile"
|
|
||||||
/>
|
|
||||||
<ModalWrapper ref="modalConfirmUnlock" header="Are you sure you want to unlock this instance?">
|
|
||||||
<div class="modal-delete">
|
|
||||||
<div
|
|
||||||
class="markdown-body"
|
|
||||||
v-html="
|
|
||||||
'If you proceed, you will not be able to re-lock it without using the `Reinstall modpack` button.'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div class="input-group push-right">
|
|
||||||
<button class="btn" @click="$refs.modalConfirmUnlock.hide()">
|
|
||||||
<XIcon />
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" @click="unlockProfile">
|
|
||||||
<LockIcon />
|
|
||||||
Unlock
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
|
|
||||||
<ModalWrapper ref="modalConfirmUnpair" header="Are you sure you want to unpair this instance?">
|
|
||||||
<div class="modal-delete">
|
|
||||||
<div
|
|
||||||
class="markdown-body"
|
|
||||||
v-html="
|
|
||||||
'If you proceed, you will not be able to re-pair it without creating an entirely new instance.'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div class="input-group push-right">
|
|
||||||
<button class="btn" @click="$refs.modalConfirmUnpair.hide()">
|
|
||||||
<XIcon />
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" @click="unpairProfile">
|
|
||||||
<XIcon />
|
|
||||||
Unpair
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
|
|
||||||
<ModalWrapper ref="changeVersionsModal" header="Change instance versions">
|
|
||||||
<div class="change-versions-modal universal-body">
|
|
||||||
<div class="input-row">
|
|
||||||
<p class="input-label">Loader</p>
|
|
||||||
<Chips v-model="loader" :items="loaders" :never-empty="false" />
|
|
||||||
</div>
|
|
||||||
<div class="input-row">
|
|
||||||
<p class="input-label">Game Version</p>
|
|
||||||
<div class="versions">
|
|
||||||
<DropdownSelect
|
|
||||||
v-model="gameVersion"
|
|
||||||
:options="selectableGameVersions"
|
|
||||||
name="Game Version Dropdown"
|
|
||||||
render-up
|
|
||||||
/>
|
|
||||||
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="loader !== 'vanilla'" class="input-row">
|
|
||||||
<p class="input-label">Loader Version</p>
|
|
||||||
<DropdownSelect
|
|
||||||
:model-value="selectableLoaderVersions[loaderVersionIndex]"
|
|
||||||
:options="selectableLoaderVersions"
|
|
||||||
:display-name="(option) => option?.id"
|
|
||||||
name="Version selector"
|
|
||||||
render-up
|
|
||||||
@change="(value) => (loaderVersionIndex = value.index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="push-right input-group">
|
|
||||||
<button class="btn" @click="$refs.changeVersionsModal.hide()">
|
|
||||||
<XIcon />
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
:disabled="!isValid || !isChanged || editing"
|
|
||||||
@click="saveGvLoaderEdits()"
|
|
||||||
>
|
|
||||||
<SaveIcon />
|
|
||||||
{{ editing ? 'Saving...' : 'Save changes' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
<section class="card">
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Instance</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<label for="instance-icon">
|
|
||||||
<span class="label__title">Icon</span>
|
|
||||||
</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<Avatar :src="icon ? convertFileSrc(icon) : icon" size="md" class="project__icon" />
|
|
||||||
<div class="input-stack">
|
|
||||||
<button id="instance-icon" class="btn" @click="setIcon">
|
|
||||||
<UploadIcon />
|
|
||||||
Select icon
|
|
||||||
</button>
|
|
||||||
<button :disabled="!icon" class="btn" @click="resetIcon">
|
|
||||||
<TrashIcon />
|
|
||||||
Remove icon
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label for="project-name">
|
|
||||||
<span class="label__title">Name</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="profile-name"
|
|
||||||
v-model="title"
|
|
||||||
autocomplete="off"
|
|
||||||
maxlength="80"
|
|
||||||
type="text"
|
|
||||||
:disabled="instance.linked_data"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="edit-versions">
|
|
||||||
<span class="label__title">Edit mod loader/game versions</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Allows you to change the mod loader, loader version, or game version of the instance.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
id="edit-versions"
|
|
||||||
class="btn"
|
|
||||||
:disabled="offline"
|
|
||||||
@click="$refs.changeVersionsModal.show()"
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
Edit versions
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label>
|
|
||||||
<span class="label__title">Categories</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Set the categories of this instance, for display in the library page. This is purely
|
|
||||||
cosmetic.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<multiselect
|
|
||||||
v-model="groups"
|
|
||||||
:options="availableGroups"
|
|
||||||
:multiple="true"
|
|
||||||
:searchable="true"
|
|
||||||
:show-no-results="false"
|
|
||||||
:close-on-select="false"
|
|
||||||
:clear-search-on-select="false"
|
|
||||||
:show-labels="false"
|
|
||||||
:taggable="true"
|
|
||||||
tag-placeholder="Add new category"
|
|
||||||
placeholder="Select categories..."
|
|
||||||
@tag="
|
|
||||||
(newTag) => {
|
|
||||||
groups.push(newTag.trim().substring(0, 32))
|
|
||||||
availableGroups.push(newTag.trim().substring(0, 32))
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Java</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h3>Installation</h3>
|
|
||||||
<Checkbox v-model="overrideJavaInstall" label="Override global java installations" />
|
|
||||||
<JavaSelector v-model="javaInstall" :disabled="!overrideJavaInstall" />
|
|
||||||
</div>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<div class="settings-group">
|
|
||||||
<h3>Java arguments</h3>
|
|
||||||
<Checkbox v-model="overrideJavaArgs" label="Override global java arguments" />
|
|
||||||
<input
|
|
||||||
id="java-args"
|
|
||||||
v-model="javaArgs"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!overrideJavaArgs"
|
|
||||||
type="text"
|
|
||||||
class="installation-input"
|
|
||||||
placeholder="Enter java arguments..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<h3>Environment variables</h3>
|
|
||||||
<Checkbox v-model="overrideEnvVars" label="Override global environment variables" />
|
|
||||||
<input
|
|
||||||
v-model="envVars"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!overrideEnvVars"
|
|
||||||
type="text"
|
|
||||||
class="installation-input"
|
|
||||||
placeholder="Enter environment variables..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<div class="settings-group">
|
|
||||||
<h3>Java memory</h3>
|
|
||||||
<Checkbox v-model="overrideMemorySettings" label="Override global memory settings" />
|
|
||||||
<Slider
|
|
||||||
v-model="memory.maximum"
|
|
||||||
:disabled="!overrideMemorySettings"
|
|
||||||
:min="512"
|
|
||||||
:max="maxMemory"
|
|
||||||
:step="64"
|
|
||||||
unit="mb"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Window</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<Checkbox v-model="overrideWindowSettings" label="Override global window settings" />
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="fullscreen">
|
|
||||||
<span class="label__title">Fullscreen</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Make the game start in full screen when launched (using options.txt).
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle
|
|
||||||
id="fullscreen"
|
|
||||||
:model-value="fullscreenSetting"
|
|
||||||
:checked="fullscreenSetting"
|
|
||||||
:disabled="!overrideWindowSettings"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
fullscreenSetting = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="width">
|
|
||||||
<span class="label__title">Width</span>
|
|
||||||
<span class="label__description"> The width of the game window when launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="width"
|
|
||||||
v-model="resolution[0]"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter width..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="height">
|
|
||||||
<span class="label__title">Height</span>
|
|
||||||
<span class="label__description"> The height of the game window when launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="height"
|
|
||||||
v-model="resolution[1]"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
|
||||||
type="number"
|
|
||||||
class="input"
|
|
||||||
placeholder="Enter height..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Hooks</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<Checkbox v-model="overrideHooks" label="Override global hooks" />
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="pre-launch">
|
|
||||||
<span class="label__title">Pre launch</span>
|
|
||||||
<span class="label__description"> Ran before the instance is launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="pre-launch"
|
|
||||||
v-model="hooks.pre_launch"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!overrideHooks"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter pre-launch command..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="wrapper">
|
|
||||||
<span class="label__title">Wrapper</span>
|
|
||||||
<span class="label__description"> Wrapper command for launching Minecraft. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="wrapper"
|
|
||||||
v-model="hooks.wrapper"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!overrideHooks"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter wrapper command..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="post-exit">
|
|
||||||
<span class="label__title">Post exit</span>
|
|
||||||
<span class="label__description"> Ran after the game closes. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="post-exit"
|
|
||||||
v-model="hooks.post_exit"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="!overrideHooks"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter post-exit command..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card v-if="instance.linked_data">
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Modpack</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="general-modpack-info">
|
|
||||||
<span class="label__description"> <strong>Modpack: </strong> {{ instance.name }} </span>
|
|
||||||
<span class="label__description">
|
|
||||||
<strong>Version: </strong>
|
|
||||||
{{
|
|
||||||
installedVersionData?.name != null
|
|
||||||
? installedVersionData.name.charAt(0).toUpperCase() +
|
|
||||||
installedVersionData.name.slice(1)
|
|
||||||
: getLocalVersion(props.instance.path)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="!isPackLocked" class="adjacent-input">
|
|
||||||
<Card class="unlocked-instance">
|
|
||||||
This is an unlocked instance. There may be unexpected behaviour unintended by the modpack
|
|
||||||
creator.
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div v-else class="adjacent-input">
|
|
||||||
<label for="unlock-profile">
|
|
||||||
<span class="label__title">Unlock instance</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Allows modifications to the instance, which allows you to add projects to the modpack. The
|
|
||||||
pack will remain linked, and you can still change versions. Only mods listed in the
|
|
||||||
modpack will be modified on version changes.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Button id="unlock-profile" @click="$refs.modalConfirmUnlock.show()">
|
|
||||||
<LockIcon /> Unlock
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="unpair-profile">
|
|
||||||
<span class="label__title">Unpair instance</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Removes the link to an external Modrinth modpack on the instance. This allows you to edit
|
|
||||||
modpacks you download through the browse page but you will not be able to update the
|
|
||||||
instance from a new version of a modpack if you do this.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Button id="unpair-profile" @click="$refs.modalConfirmUnpair.show()">
|
|
||||||
<XIcon /> Unpair
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="instance.linked_data.project_id" class="adjacent-input">
|
|
||||||
<label for="change-modpack-version">
|
|
||||||
<span class="label__title">Change modpack version</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Changes to another version of the modpack, allowing upgrading or downgrading. This will
|
|
||||||
replace all files marked as relevant to the modpack.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
id="change-modpack-version"
|
|
||||||
:disabled="inProgress || installing"
|
|
||||||
@click="modpackVersionModal.show()"
|
|
||||||
>
|
|
||||||
<SwapIcon />
|
|
||||||
Change modpack version
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="repair-modpack">
|
|
||||||
<span class="label__title">Reinstall modpack</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Removes all projects and reinstalls Modrinth modpack. Use this to fix unexpected behaviour
|
|
||||||
if your instance is diverging from the Modrinth modpack. This also re-locks the instance.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Button id="repair-modpack" color="highlight" :disabled="offline" @click="repairModpack">
|
|
||||||
<DownloadIcon /> Reinstall
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Instance management</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div v-if="instance.install_stage == 'installed'" class="adjacent-input">
|
|
||||||
<label for="duplicate-profile">
|
|
||||||
<span class="label__title">Duplicate instance</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Creates another copy of the instance, including saves, configs, mods, and everything.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Button
|
|
||||||
id="repair-profile"
|
|
||||||
:disabled:="installing || inProgress || offline"
|
|
||||||
@click="duplicateProfile"
|
|
||||||
>
|
|
||||||
<ClipboardCopyIcon /> Duplicate
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="repair-profile">
|
|
||||||
<span class="label__title">Repair instance</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Reinstalls Minecraft dependencies and checks for corruption. Use this if your game is not
|
|
||||||
launching due to launcher-related errors.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Button
|
|
||||||
id="repair-profile"
|
|
||||||
color="highlight"
|
|
||||||
:disabled="installing || inProgress || repairing || offline"
|
|
||||||
@click="repairProfile(true)"
|
|
||||||
>
|
|
||||||
<HammerIcon /> Repair
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="delete-profile">
|
|
||||||
<span class="label__title">Delete instance</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Fully removes a instance from the disk. Be careful, as once you delete a instance there is
|
|
||||||
no way to recover it.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Button
|
|
||||||
id="delete-profile"
|
|
||||||
color="danger"
|
|
||||||
:disabled="removing"
|
|
||||||
@click="$refs.modal_confirm.show()"
|
|
||||||
>
|
|
||||||
<TrashIcon /> Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<ModpackVersionModal
|
|
||||||
v-if="instance.linked_data"
|
|
||||||
ref="modpackVersionModal"
|
|
||||||
:instance="instance"
|
|
||||||
:versions="props.versions"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
TrashIcon,
|
|
||||||
UploadIcon,
|
|
||||||
EditIcon,
|
|
||||||
XIcon,
|
|
||||||
SaveIcon,
|
|
||||||
LockIcon,
|
|
||||||
HammerIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
ClipboardCopyIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { Button, Toggle, Card, Slider, Checkbox, Avatar, Chips, DropdownSelect } from '@modrinth/ui'
|
|
||||||
import { SwapIcon } from '@/assets/icons'
|
|
||||||
|
|
||||||
import { Multiselect } from 'vue-multiselect'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import {
|
|
||||||
duplicate,
|
|
||||||
edit,
|
|
||||||
edit_icon,
|
|
||||||
get_optimal_jre_key,
|
|
||||||
install,
|
|
||||||
list,
|
|
||||||
remove,
|
|
||||||
update_repair_modrinth,
|
|
||||||
} from '@/helpers/profile.js'
|
|
||||||
import { computed, readonly, ref, shallowRef, watch } from 'vue'
|
|
||||||
import { get_max_memory } from '@/helpers/jre.js'
|
|
||||||
import { get } from '@/helpers/settings.js'
|
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { get_loader_versions } from '@/helpers/metadata.js'
|
|
||||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
|
|
||||||
const breadcrumbs = useBreadcrumbs()
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
instance: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
offline: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = ref(props.instance.name)
|
|
||||||
const icon = ref(props.instance.icon_path)
|
|
||||||
const groups = ref(props.instance.groups)
|
|
||||||
|
|
||||||
const modpackVersionModal = ref(null)
|
|
||||||
|
|
||||||
const instancesList = await list()
|
|
||||||
const availableGroups = ref([
|
|
||||||
...new Set(
|
|
||||||
instancesList.reduce((acc, obj) => {
|
|
||||||
return acc.concat(obj.groups)
|
|
||||||
}, []),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
|
|
||||||
async function resetIcon() {
|
|
||||||
icon.value = null
|
|
||||||
await edit_icon(props.instance.path, null).catch(handleError)
|
|
||||||
trackEvent('InstanceRemoveIcon')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setIcon() {
|
|
||||||
const value = await open({
|
|
||||||
multiple: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Image',
|
|
||||||
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!value) return
|
|
||||||
|
|
||||||
icon.value = value.path ?? value
|
|
||||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
|
||||||
|
|
||||||
trackEvent('InstanceSetIcon')
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalSettings = await get().catch(handleError)
|
|
||||||
|
|
||||||
const modalConfirmUnlock = ref(null)
|
|
||||||
const modalConfirmUnpair = ref(null)
|
|
||||||
|
|
||||||
const overrideJavaInstall = ref(!!props.instance.java_path)
|
|
||||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
|
||||||
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
|
||||||
|
|
||||||
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
|
||||||
const javaArgs = ref(
|
|
||||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
|
||||||
)
|
|
||||||
|
|
||||||
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
|
||||||
const envVars = ref(
|
|
||||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
|
||||||
.map((x) => x.join('='))
|
|
||||||
.join(' '),
|
|
||||||
)
|
|
||||||
|
|
||||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
|
||||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
|
||||||
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
|
||||||
|
|
||||||
const overrideWindowSettings = ref(
|
|
||||||
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
|
||||||
)
|
|
||||||
const resolution = ref(props.instance.game_resolution ?? globalSettings.game_resolution)
|
|
||||||
const overrideHooks = ref(
|
|
||||||
props.instance.hooks.pre_launch || props.instance.hooks.wrapper || props.instance.hooks.post_exit,
|
|
||||||
)
|
|
||||||
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
|
||||||
|
|
||||||
const fullscreenSetting = ref(!!props.instance.force_fullscreen)
|
|
||||||
|
|
||||||
const unlinkModpack = ref(false)
|
|
||||||
|
|
||||||
const inProgress = ref(false)
|
|
||||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
|
||||||
const installedVersion = computed(() => props.instance?.linked_data?.version_id)
|
|
||||||
const installedVersionData = computed(() => {
|
|
||||||
if (!installedVersion.value) return null
|
|
||||||
return props.versions.find((version) => version.id === installedVersion.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
[
|
|
||||||
title,
|
|
||||||
groups,
|
|
||||||
groups,
|
|
||||||
overrideJavaInstall,
|
|
||||||
javaInstall,
|
|
||||||
overrideJavaArgs,
|
|
||||||
javaArgs,
|
|
||||||
overrideEnvVars,
|
|
||||||
envVars,
|
|
||||||
overrideMemorySettings,
|
|
||||||
memory,
|
|
||||||
overrideWindowSettings,
|
|
||||||
resolution,
|
|
||||||
fullscreenSetting,
|
|
||||||
overrideHooks,
|
|
||||||
hooks,
|
|
||||||
unlinkModpack,
|
|
||||||
],
|
|
||||||
async () => {
|
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
const getLocalVersion = (path) => {
|
|
||||||
const pathSlice = path.split(' ').slice(-1).toString()
|
|
||||||
// If the path ends in (1), (2), etc. it's a duplicate instance and no version can be obtained.
|
|
||||||
if (/^\(\d\)/.test(pathSlice)) {
|
|
||||||
return 'Unknown'
|
|
||||||
}
|
|
||||||
return pathSlice
|
|
||||||
}
|
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
|
||||||
const editProfile = {
|
|
||||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
|
||||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
|
||||||
loader_version: props.instance.loader_version,
|
|
||||||
linked_data: props.instance.linked_data,
|
|
||||||
java: {},
|
|
||||||
hooks: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideJavaInstall.value) {
|
|
||||||
if (javaInstall.value.path !== '') {
|
|
||||||
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideJavaArgs.value) {
|
|
||||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideEnvVars.value) {
|
|
||||||
editProfile.custom_env_vars = envVars.value
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideMemorySettings.value) {
|
|
||||||
editProfile.memory = memory.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideWindowSettings.value) {
|
|
||||||
editProfile.force_fullscreen = fullscreenSetting.value
|
|
||||||
|
|
||||||
if (!fullscreenSetting.value) {
|
|
||||||
editProfile.game_resolution = resolution.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideHooks.value) {
|
|
||||||
editProfile.hooks = hooks.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unlinkModpack.value) {
|
|
||||||
editProfile.linked_data = null
|
|
||||||
}
|
|
||||||
|
|
||||||
breadcrumbs.setName('Instance', editProfile.name)
|
|
||||||
|
|
||||||
return editProfile
|
|
||||||
})
|
|
||||||
|
|
||||||
const repairing = ref(false)
|
|
||||||
|
|
||||||
async function duplicateProfile() {
|
|
||||||
await duplicate(props.instance.path).catch(handleError)
|
|
||||||
trackEvent('InstanceDuplicate', {
|
|
||||||
loader: props.instance.loader,
|
|
||||||
game_version: props.instance.game_version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function repairProfile(force) {
|
|
||||||
repairing.value = true
|
|
||||||
await install(props.instance.path, force).catch(handleError)
|
|
||||||
repairing.value = false
|
|
||||||
|
|
||||||
trackEvent('InstanceRepair', {
|
|
||||||
loader: props.instance.loader,
|
|
||||||
game_version: props.instance.game_version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unpairProfile() {
|
|
||||||
const editProfile = props.instance
|
|
||||||
editProfile.linked_data = null
|
|
||||||
await edit(props.instance.path, editProfile)
|
|
||||||
installedVersion.value = null
|
|
||||||
installedVersionData.value = null
|
|
||||||
modalConfirmUnpair.value.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unlockProfile() {
|
|
||||||
const editProfile = props.instance
|
|
||||||
editProfile.linked_data.locked = false
|
|
||||||
await edit(props.instance.path, editProfile)
|
|
||||||
modalConfirmUnlock.value.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPackLocked = computed(() => {
|
|
||||||
return props.instance.linked_data && props.instance.linked_data.locked
|
|
||||||
})
|
|
||||||
|
|
||||||
async function repairModpack() {
|
|
||||||
inProgress.value = true
|
|
||||||
await update_repair_modrinth(props.instance.path).catch(handleError)
|
|
||||||
inProgress.value = false
|
|
||||||
|
|
||||||
trackEvent('InstanceRepair', {
|
|
||||||
loader: props.instance.loader,
|
|
||||||
game_version: props.instance.game_version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removing = ref(false)
|
|
||||||
async function removeProfile() {
|
|
||||||
removing.value = true
|
|
||||||
await remove(props.instance.path).catch(handleError)
|
|
||||||
removing.value = false
|
|
||||||
|
|
||||||
trackEvent('InstanceRemove', {
|
|
||||||
loader: props.instance.loader,
|
|
||||||
game_version: props.instance.game_version,
|
|
||||||
})
|
|
||||||
|
|
||||||
await router.push({ path: '/' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeVersionsModal = ref(null)
|
|
||||||
const showSnapshots = ref(false)
|
|
||||||
|
|
||||||
const [
|
|
||||||
fabric_versions,
|
|
||||||
forge_versions,
|
|
||||||
quilt_versions,
|
|
||||||
neoforge_versions,
|
|
||||||
all_game_versions,
|
|
||||||
loaders,
|
|
||||||
] = await Promise.all([
|
|
||||||
get_loader_versions('fabric').then(shallowRef).catch(handleError),
|
|
||||||
get_loader_versions('forge').then(shallowRef).catch(handleError),
|
|
||||||
get_loader_versions('quilt').then(shallowRef).catch(handleError),
|
|
||||||
get_loader_versions('neo').then(shallowRef).catch(handleError),
|
|
||||||
get_game_versions().then(shallowRef).catch(handleError),
|
|
||||||
get_loaders()
|
|
||||||
.then((value) =>
|
|
||||||
value
|
|
||||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
|
||||||
.map((item) => item.name.toLowerCase()),
|
|
||||||
)
|
|
||||||
.then(ref)
|
|
||||||
.catch(handleError),
|
|
||||||
])
|
|
||||||
loaders.value.unshift('vanilla')
|
|
||||||
|
|
||||||
const loader = ref(props.instance.loader)
|
|
||||||
const gameVersion = ref(props.instance.game_version)
|
|
||||||
const selectableGameVersions = computed(() => {
|
|
||||||
return all_game_versions.value
|
|
||||||
.filter((item) => {
|
|
||||||
let defaultVal = item.version_type === 'release' || showSnapshots.value
|
|
||||||
if (loader.value === 'fabric') {
|
|
||||||
defaultVal &= fabric_versions.value.gameVersions.some((x) => item.version === x.id)
|
|
||||||
} else if (loader.value === 'forge') {
|
|
||||||
defaultVal &= forge_versions.value.gameVersions.some((x) => item.version === x.id)
|
|
||||||
} else if (loader.value === 'quilt') {
|
|
||||||
defaultVal &= quilt_versions.value.gameVersions.some((x) => item.version === x.id)
|
|
||||||
} else if (loader.value === 'neoforge') {
|
|
||||||
defaultVal &= neoforge_versions.value.gameVersions.some((x) => item.version === x.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultVal
|
|
||||||
})
|
|
||||||
.map((item) => item.version)
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectableLoaderVersions = computed(() => {
|
|
||||||
if (gameVersion.value) {
|
|
||||||
if (loader.value === 'fabric') {
|
|
||||||
return fabric_versions.value.gameVersions[0].loaders
|
|
||||||
} else if (loader.value === 'forge') {
|
|
||||||
return forge_versions.value.gameVersions.find((item) => item.id === gameVersion.value).loaders
|
|
||||||
} else if (loader.value === 'quilt') {
|
|
||||||
return quilt_versions.value.gameVersions[0].loaders
|
|
||||||
} else if (loader.value === 'neoforge') {
|
|
||||||
return neoforge_versions.value.gameVersions.find((item) => item.id === gameVersion.value)
|
|
||||||
.loaders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
const loaderVersionIndex = ref(
|
|
||||||
selectableLoaderVersions.value.findIndex((x) => x.id === props.instance.loader_version),
|
|
||||||
)
|
|
||||||
|
|
||||||
const isValid = computed(() => {
|
|
||||||
return (
|
|
||||||
selectableGameVersions.value.includes(gameVersion.value) &&
|
|
||||||
(loaderVersionIndex.value >= 0 || loader.value === 'vanilla')
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isChanged = computed(() => {
|
|
||||||
return (
|
|
||||||
loader.value !== props.instance.loader ||
|
|
||||||
gameVersion.value !== props.instance.game_version ||
|
|
||||||
(loaderVersionIndex.value >= 0 &&
|
|
||||||
selectableLoaderVersions.value[loaderVersionIndex.value].id !== props.instance.loader_version)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(loader, () => (loaderVersionIndex.value = 0))
|
|
||||||
|
|
||||||
const editing = ref(false)
|
|
||||||
async function saveGvLoaderEdits() {
|
|
||||||
editing.value = true
|
|
||||||
|
|
||||||
const editProfile = editProfileObject.value
|
|
||||||
editProfile.loader = loader.value
|
|
||||||
editProfile.game_version = gameVersion.value
|
|
||||||
|
|
||||||
if (loader.value !== 'vanilla') {
|
|
||||||
editProfile.loader_version = selectableLoaderVersions.value[loaderVersionIndex.value].id
|
|
||||||
} else {
|
|
||||||
loaderVersionIndex.value = -1
|
|
||||||
}
|
|
||||||
await edit(props.instance.path, editProfile).catch(handleError)
|
|
||||||
await repairProfile(false)
|
|
||||||
|
|
||||||
editing.value = false
|
|
||||||
changeVersionsModal.value.hide()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.change-versions-modal {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
:deep(.animated-dropdown .options) {
|
|
||||||
max-height: 13.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-label {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bolder;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.installation-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(button.checkbox) {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unlocked-instance {
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-delete {
|
|
||||||
padding: var(--gap-lg);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-label {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-text {
|
|
||||||
padding-right: 0.25ch;
|
|
||||||
margin: 0 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-input {
|
|
||||||
input {
|
|
||||||
width: 20rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import Index from './Index.vue'
|
import Index from './Index.vue'
|
||||||
import Mods from './Mods.vue'
|
import Mods from './Mods.vue'
|
||||||
import Options from './Options.vue'
|
|
||||||
import Logs from './Logs.vue'
|
import Logs from './Logs.vue'
|
||||||
|
|
||||||
export { Index, Mods, Options, Logs }
|
export { Index, Mods, Logs }
|
||||||
|
|||||||
@ -36,7 +36,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 flex flex-col gap-3">
|
<div class="p-6 flex flex-col gap-3">
|
||||||
<h1 class="m-0 text-2xl">Library</h1>
|
<h1 class="m-0 text-2xl hidden">Library</h1>
|
||||||
<NavTabs
|
<NavTabs
|
||||||
:links="[
|
:links="[
|
||||||
{ label: 'All instances', href: `/library` },
|
{ label: 'All instances', href: `/library` },
|
||||||
|
|||||||
@ -25,7 +25,10 @@
|
|||||||
<div class="flex flex-col gap-4 p-6">
|
<div class="flex flex-col gap-4 p-6">
|
||||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||||
<template v-if="data">
|
<template v-if="data">
|
||||||
<Teleport v-if="themeStore.featureFlag_projectBackground" to="#background-teleport-target">
|
<Teleport
|
||||||
|
v-if="themeStore.featureFlags.project_background"
|
||||||
|
to="#background-teleport-target"
|
||||||
|
>
|
||||||
<ProjectBackgroundGradient :project="data" />
|
<ProjectBackgroundGradient :project="data" />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<ProjectHeader :project="data">
|
<ProjectHeader :project="data">
|
||||||
|
|||||||
@ -124,15 +124,6 @@ export default new createRouter({
|
|||||||
breadcrumb: [{ name: '?Instance' }],
|
breadcrumb: [{ name: '?Instance' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'options',
|
|
||||||
name: 'Options',
|
|
||||||
component: Instance.Options,
|
|
||||||
meta: {
|
|
||||||
useRootContext: true,
|
|
||||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Options' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { defineStore } from 'pinia'
|
|||||||
|
|
||||||
export const useTheming = defineStore('themeStore', {
|
export const useTheming = defineStore('themeStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
themeOptions: ['dark', 'light', 'oled'],
|
themeOptions: ['dark', 'light', 'oled', 'system'],
|
||||||
advancedRendering: true,
|
advancedRendering: true,
|
||||||
selectedTheme: 'dark',
|
selectedTheme: 'dark',
|
||||||
|
toggleSidebar: false,
|
||||||
|
|
||||||
devMode: false,
|
devMode: false,
|
||||||
featureFlag_pagePath: false,
|
featureFlags: {},
|
||||||
featureFlag_projectBackground: false,
|
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setThemeState(newTheme) {
|
setThemeState(newTheme) {
|
||||||
@ -21,7 +21,18 @@ export const useTheming = defineStore('themeStore', {
|
|||||||
for (const theme of this.themeOptions) {
|
for (const theme of this.themeOptions) {
|
||||||
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
||||||
}
|
}
|
||||||
document.getElementsByTagName('html')[0].classList.add(`${this.selectedTheme}-mode`)
|
|
||||||
|
let theme = this.selectedTheme
|
||||||
|
if (this.selectedTheme === 'system') {
|
||||||
|
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
if (darkThemeMq.matches) {
|
||||||
|
theme = 'dark'
|
||||||
|
} else {
|
||||||
|
theme = 'light'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementsByTagName('html')[0].classList.add(`${theme}-mode`)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.9.0-1"
|
version = "0.9.0-2"
|
||||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/modrinth/code/apps/app/"
|
repository = "https://github.com/modrinth/code/apps/app/"
|
||||||
@ -18,7 +18,6 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_with = "3.0.0"
|
serde_with = "3.0.0"
|
||||||
|
|
||||||
tauri = { version = "2.1.1", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
tauri = { version = "2.1.1", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||||
# tauri = { git = "https://github.com/modrinth/tauri", rev = "67911d5", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
|
||||||
tauri-plugin-window-state = "2.2.0"
|
tauri-plugin-window-state = "2.2.0"
|
||||||
tauri-plugin-deep-link = "2.2.0"
|
tauri-plugin-deep-link = "2.2.0"
|
||||||
tauri-plugin-os = "2.2.0"
|
tauri-plugin-os = "2.2.0"
|
||||||
|
|||||||
@ -8,6 +8,7 @@ use tokio::sync::RwLock;
|
|||||||
|
|
||||||
pub struct AdsState {
|
pub struct AdsState {
|
||||||
pub shown: bool,
|
pub shown: bool,
|
||||||
|
pub modal_shown: bool,
|
||||||
pub last_click: Option<Instant>,
|
pub last_click: Option<Instant>,
|
||||||
pub malicious_origins: HashSet<String>,
|
pub malicious_origins: HashSet<String>,
|
||||||
}
|
}
|
||||||
@ -19,6 +20,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.setup(|app, _api| {
|
.setup(|app, _api| {
|
||||||
app.manage(RwLock::new(AdsState {
|
app.manage(RwLock::new(AdsState {
|
||||||
shown: true,
|
shown: true,
|
||||||
|
modal_shown: false,
|
||||||
last_click: None,
|
last_click: None,
|
||||||
malicious_origins: HashSet::new(),
|
malicious_origins: HashSet::new(),
|
||||||
}));
|
}));
|
||||||
@ -86,6 +88,10 @@ pub async fn init_ads_window<R: Runtime>(
|
|||||||
state.shown = true;
|
state.shown = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.modal_shown {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok((position, size)) = get_webview_position(&app, dpr) {
|
if let Ok((position, size)) = get_webview_position(&app, dpr) {
|
||||||
if let Some(webview) = app.webviews().get("ads-window") {
|
if let Some(webview) = app.webviews().get("ads-window") {
|
||||||
if state.shown {
|
if state.shown {
|
||||||
@ -133,7 +139,9 @@ pub async fn show_ads_window<R: Runtime>(
|
|||||||
) -> crate::api::Result<()> {
|
) -> crate::api::Result<()> {
|
||||||
if let Some(webview) = app.webviews().get("ads-window") {
|
if let Some(webview) = app.webviews().get("ads-window") {
|
||||||
let state = app.state::<RwLock<AdsState>>();
|
let state = app.state::<RwLock<AdsState>>();
|
||||||
let state = state.read().await;
|
let mut state = state.write().await;
|
||||||
|
|
||||||
|
state.modal_shown = false;
|
||||||
|
|
||||||
if state.shown {
|
if state.shown {
|
||||||
let (position, size) = get_webview_position(&app, dpr)?;
|
let (position, size) = get_webview_position(&app, dpr)?;
|
||||||
@ -151,11 +159,13 @@ pub async fn hide_ads_window<R: Runtime>(
|
|||||||
reset: Option<bool>,
|
reset: Option<bool>,
|
||||||
) -> crate::api::Result<()> {
|
) -> crate::api::Result<()> {
|
||||||
if let Some(webview) = app.webviews().get("ads-window") {
|
if let Some(webview) = app.webviews().get("ads-window") {
|
||||||
if reset.unwrap_or(false) {
|
let state = app.state::<RwLock<AdsState>>();
|
||||||
let state = app.state::<RwLock<AdsState>>();
|
let mut state = state.write().await;
|
||||||
let mut state = state.write().await;
|
|
||||||
|
|
||||||
|
if reset.unwrap_or(false) {
|
||||||
state.shown = false;
|
state.shown = false;
|
||||||
|
} else {
|
||||||
|
state.modal_shown = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = webview.set_position(PhysicalPosition::new(-1000, -1000));
|
let _ = webview.set_position(PhysicalPosition::new(-1000, -1000));
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use tauri::{
|
|||||||
}; // 0.8
|
}; // 0.8
|
||||||
|
|
||||||
const WINDOW_CONTROL_PAD_X: f64 = 9.0;
|
const WINDOW_CONTROL_PAD_X: f64 = 9.0;
|
||||||
const WINDOW_CONTROL_PAD_Y: f64 = 16.0;
|
const WINDOW_CONTROL_PAD_Y: f64 = 10.0;
|
||||||
|
|
||||||
struct UnsafeWindowHandle(*mut std::ffi::c_void);
|
struct UnsafeWindowHandle(*mut std::ffi::c_void);
|
||||||
unsafe impl Send for UnsafeWindowHandle {}
|
unsafe impl Send for UnsafeWindowHandle {}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ extern crate objc;
|
|||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||||
|
tracing::info!("Initializing app event state...");
|
||||||
theseus::EventState::init(app.clone()).await?;
|
theseus::EventState::init(app.clone()).await?;
|
||||||
|
|
||||||
#[cfg(feature = "updater")]
|
#[cfg(feature = "updater")]
|
||||||
@ -35,6 +36,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
|||||||
|
|
||||||
let update_fut = updater.check();
|
let update_fut = updater.check();
|
||||||
|
|
||||||
|
tracing::info!("Initializing app state...");
|
||||||
State::init().await?;
|
State::init().await?;
|
||||||
|
|
||||||
let check_bar = theseus::init_loading(
|
let check_bar = theseus::init_loading(
|
||||||
@ -44,6 +46,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
tracing::info!("Checking for updates...");
|
||||||
let update = update_fut.await;
|
let update = update_fut.await;
|
||||||
|
|
||||||
drop(check_bar);
|
drop(check_bar);
|
||||||
@ -88,6 +91,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
|||||||
State::init().await?;
|
State::init().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("Finished checking for updates!");
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
app.asset_protocol_scope()
|
app.asset_protocol_scope()
|
||||||
.allow_directory(state.directories.caches_dir(), true)?;
|
.allow_directory(state.directories.caches_dir(), true)?;
|
||||||
@ -169,19 +173,19 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder = builder
|
builder = builder
|
||||||
// .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||||
// if let Some(payload) = args.get(1) {
|
if let Some(payload) = args.get(1) {
|
||||||
// tracing::info!("Handling deep link from arg {payload}");
|
tracing::info!("Handling deep link from arg {payload}");
|
||||||
// let payload = payload.clone();
|
let payload = payload.clone();
|
||||||
// tauri::async_runtime::spawn(api::utils::handle_command(
|
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||||
// payload,
|
payload,
|
||||||
// ));
|
));
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// if let Some(win) = app.get_window("main") {
|
if let Some(win) = app.get_window("main") {
|
||||||
// let _ = win.set_focus();
|
let _ = win.set_focus();
|
||||||
// }
|
}
|
||||||
// }))
|
}))
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
@ -274,6 +278,7 @@ fn main() {
|
|||||||
builder = builder.plugin(macos::window_ext::init());
|
builder = builder.plugin(macos::window_ext::init());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::info!("Initializing app...");
|
||||||
let app = builder.build(tauri::generate_context!());
|
let app = builder.build(tauri::generate_context!());
|
||||||
|
|
||||||
match app {
|
match app {
|
||||||
@ -335,6 +340,7 @@ fn main() {
|
|||||||
.show_alert()
|
.show_alert()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
tracing::error!("Error while running tauri application: {:?}", e);
|
||||||
panic!("{1}: {:?}", e, "error while running tauri application")
|
panic!("{1}: {:?}", e, "error while running tauri application")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"productName": "Modrinth App",
|
"productName": "Modrinth App",
|
||||||
"version": "0.9.0-1",
|
"version": "0.9.0-2",
|
||||||
"mainBinaryName": "Modrinth App",
|
"mainBinaryName": "Modrinth App",
|
||||||
"identifier": "ModrinthApp",
|
"identifier": "ModrinthApp",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|||||||
@ -373,7 +373,7 @@ export default defineNuxtComponent({
|
|||||||
clear: false,
|
clear: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
commonMessages
|
commonMessages,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head: {
|
head: {
|
||||||
|
|||||||
@ -311,7 +311,6 @@ import {
|
|||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
NewProjectCard,
|
NewProjectCard,
|
||||||
SearchFilterControl,
|
SearchFilterControl,
|
||||||
ContentPageHeader,
|
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import { CheckIcon, DownloadIcon, GameIcon, LeftArrowIcon, XIcon } from "@modrinth/assets";
|
import { CheckIcon, DownloadIcon, GameIcon, LeftArrowIcon, XIcon } from "@modrinth/assets";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|||||||
@ -22,7 +22,11 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "1397c1825096fb402cdd3b5dae8cd3910b1719f433a0c34d40415dd7681ab272"
|
"hash": "1397c1825096fb402cdd3b5dae8cd3910b1719f433a0c34d40415dd7681ab272"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,12 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "18881c0c2ec1b0cc73fa13b4c242dfc577061b92479ce96ffb30a457939b5ffe"
|
"hash": "18881c0c2ec1b0cc73fa13b4c242dfc577061b92479ce96ffb30a457939b5ffe"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,12 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "265f9c9ad992da0aeaf69c3f0077b54a186b98796ec549c9d891089ea33cf3fc"
|
"hash": "265f9c9ad992da0aeaf69c3f0077b54a186b98796ec549c9d891089ea33cf3fc"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,13 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 3
|
"Right": 3
|
||||||
},
|
},
|
||||||
"nullable": [false, false, null, true, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "28b3e3132d75e551c1fa14b8d3be36adca581f8ad1b90f85d3ec3d92ec61e65e"
|
"hash": "28b3e3132d75e551c1fa14b8d3be36adca581f8ad1b90f85d3ec3d92ec61e65e"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,12 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "6d7ebc0f233dc730fa8c99c750421065f5e35f321954a9d5ae9cde907d5ce823"
|
"hash": "6d7ebc0f233dc730fa8c99c750421065f5e35f321954a9d5ae9cde907d5ce823"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,13 +41,22 @@
|
|||||||
{
|
{
|
||||||
"name": "display_claims!: serde_json::Value",
|
"name": "display_claims!: serde_json::Value",
|
||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"type_info": "Text"
|
"type_info": "Null"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false, false, false, false, false, null]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "6e3fa492c085ebb8e7280dd4d55cdcf73da199ea6ac05ee3ee798ece80d877cf"
|
"hash": "6e3fa492c085ebb8e7280dd4d55cdcf73da199ea6ac05ee3ee798ece80d877cf"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,14 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false, false, false, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "727e3e1bc8625bbcb833920059bb8cea926ac6c65d613904eff1d740df30acda"
|
"hash": "727e3e1bc8625bbcb833920059bb8cea926ac6c65d613904eff1d740df30acda"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"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 personalized_ads = $11,\n\n onboarded = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25\n ",
|
"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 personalized_ads = $11,\n\n onboarded = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25,\n\n toggle_sidebar = $26,\n feature_flags = $27\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 25
|
"Right": 27
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "26e3ed8680f6c492b03b458aabfb3f94fddc753b343ef705263188945d0e578d"
|
"hash": "759e4ffe30ebc4f8602256cb419ef15732d84bcebb9ca15225dbabdc0f46ba2d"
|
||||||
}
|
}
|
||||||
@ -37,7 +37,14 @@
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [false, false, false, false, false, false]
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"hash": "bf7d47350092d87c478009adaab131168e87bb37aa65c2156ad2cb6198426d8c"
|
"hash": "bf7d47350092d87c478009adaab131168e87bb37aa65c2156ad2cb6198426d8c"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"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, personalized_ads,\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 ",
|
"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, personalized_ads,\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, json(feature_flags) feature_flags, toggle_sidebar\n FROM settings\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -127,6 +127,16 @@
|
|||||||
"name": "migrated",
|
"name": "migrated",
|
||||||
"ordinal": 24,
|
"ordinal": 24,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "feature_flags",
|
||||||
|
"ordinal": 25,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toggle_sidebar",
|
||||||
|
"ordinal": 26,
|
||||||
|
"type_info": "Integer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -157,8 +167,10 @@
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "8e19c9cdb0aaa48509724e82f6e8f212c9cd2112fdba77cfeee206025af47761"
|
"hash": "d90a2f2f823fc546661a94af07249758c5ca82db396268bca5087bac88f733d9"
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "0.9.0-1"
|
version = "0.9.0-2"
|
||||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE settings ADD COLUMN toggle_sidebar INTEGER NOT NULL DEFAULT FALSE;
|
||||||
|
ALTER TABLE settings ADD COLUMN feature_flags JSONB NOT NULL default '{}';
|
||||||
@ -474,7 +474,11 @@ impl CacheValue {
|
|||||||
| CacheValue::DonationPlatforms(_) => DEFAULT_ID.to_string(),
|
| CacheValue::DonationPlatforms(_) => DEFAULT_ID.to_string(),
|
||||||
|
|
||||||
CacheValue::FileHash(hash) => {
|
CacheValue::FileHash(hash) => {
|
||||||
format!("{}-{}", hash.size, hash.path.replace(".disabled", ""))
|
format!(
|
||||||
|
"{}-{}",
|
||||||
|
hash.size,
|
||||||
|
hash.path.trim_end_matches(".disabled")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
CacheValue::FileUpdate(hash) => {
|
CacheValue::FileUpdate(hash) => {
|
||||||
format!("{}-{}-{}", hash.hash, hash.loader, hash.game_version)
|
format!("{}-{}-{}", hash.hash, hash.loader, hash.game_version)
|
||||||
|
|||||||
@ -122,10 +122,12 @@ impl State {
|
|||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn initialize_state() -> crate::Result<Arc<Self>> {
|
async fn initialize_state() -> crate::Result<Arc<Self>> {
|
||||||
|
tracing::info!("Connecting to app database");
|
||||||
let pool = db::connect().await?;
|
let pool = db::connect().await?;
|
||||||
|
|
||||||
legacy_converter::migrate_legacy_data(&pool).await?;
|
legacy_converter::migrate_legacy_data(&pool).await?;
|
||||||
|
|
||||||
|
tracing::info!("Fetching app settings");
|
||||||
let mut settings = Settings::get(&pool).await?;
|
let mut settings = Settings::get(&pool).await?;
|
||||||
|
|
||||||
let fetch_semaphore =
|
let fetch_semaphore =
|
||||||
@ -135,6 +137,7 @@ impl State {
|
|||||||
let api_semaphore =
|
let api_semaphore =
|
||||||
FetchSemaphore(Semaphore::new(settings.max_concurrent_downloads));
|
FetchSemaphore(Semaphore::new(settings.max_concurrent_downloads));
|
||||||
|
|
||||||
|
tracing::info!("Initializing directories");
|
||||||
DirectoryInfo::move_launcher_directory(
|
DirectoryInfo::move_launcher_directory(
|
||||||
&mut settings,
|
&mut settings,
|
||||||
&pool,
|
&pool,
|
||||||
@ -145,6 +148,7 @@ impl State {
|
|||||||
|
|
||||||
let discord_rpc = DiscordGuard::init()?;
|
let discord_rpc = DiscordGuard::init()?;
|
||||||
|
|
||||||
|
tracing::info!("Initializing file watcher");
|
||||||
let file_watcher = fs_watcher::init_watcher().await?;
|
let file_watcher = fs_watcher::init_watcher().await?;
|
||||||
fs_watcher::watch_profiles_init(&file_watcher, &directories).await;
|
fs_watcher::watch_profiles_init(&file_watcher, &directories).await;
|
||||||
|
|
||||||
|
|||||||
@ -664,7 +664,7 @@ impl Profile {
|
|||||||
path: format!(
|
path: format!(
|
||||||
"{}/{folder}/{}",
|
"{}/{folder}/{}",
|
||||||
self.path,
|
self.path,
|
||||||
file_name.replace(".disabled", "")
|
file_name.trim_end_matches(".disabled")
|
||||||
),
|
),
|
||||||
file_name: file_name.to_string(),
|
file_name: file_name.to_string(),
|
||||||
project_type,
|
project_type,
|
||||||
@ -725,8 +725,9 @@ impl Profile {
|
|||||||
let info_index = file_info.iter().position(|x| x.hash == hash.hash);
|
let info_index = file_info.iter().position(|x| x.hash == hash.hash);
|
||||||
let file = info_index.map(|x| file_info.remove(x));
|
let file = info_index.map(|x| file_info.remove(x));
|
||||||
|
|
||||||
if let Some(initial_file_index) =
|
if let Some(initial_file_index) = keys
|
||||||
keys.iter().position(|x| x.path == hash.path)
|
.iter()
|
||||||
|
.position(|x| x.path == hash.path.trim_end_matches(".disabled"))
|
||||||
{
|
{
|
||||||
let initial_file = keys.remove(initial_file_index);
|
let initial_file = keys.remove(initial_file_index);
|
||||||
|
|
||||||
@ -890,7 +891,7 @@ impl Profile {
|
|||||||
let path = crate::api::profile::get_full_path(profile_path).await?;
|
let path = crate::api::profile::get_full_path(profile_path).await?;
|
||||||
|
|
||||||
let new_path = if project_path.ends_with(".disabled") {
|
let new_path = if project_path.ends_with(".disabled") {
|
||||||
project_path.replace(".disabled", "")
|
project_path.trim_end_matches(".disabled").to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{project_path}.disabled")
|
format!("{project_path}.disabled")
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
//! Theseus settings file
|
//! Theseus settings file
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
/// Global Theseus settings
|
/// Global Theseus settings
|
||||||
@ -13,10 +15,10 @@ pub struct Settings {
|
|||||||
pub collapsed_navigation: bool,
|
pub collapsed_navigation: bool,
|
||||||
pub advanced_rendering: bool,
|
pub advanced_rendering: bool,
|
||||||
pub native_decorations: bool,
|
pub native_decorations: bool,
|
||||||
|
pub toggle_sidebar: bool,
|
||||||
|
|
||||||
pub telemetry: bool,
|
pub telemetry: bool,
|
||||||
pub discord_rpc: bool,
|
pub discord_rpc: bool,
|
||||||
pub developer_mode: bool,
|
|
||||||
pub personalized_ads: bool,
|
pub personalized_ads: bool,
|
||||||
|
|
||||||
pub onboarded: bool,
|
pub onboarded: bool,
|
||||||
@ -32,6 +34,16 @@ pub struct Settings {
|
|||||||
pub custom_dir: Option<String>,
|
pub custom_dir: Option<String>,
|
||||||
pub prev_custom_dir: Option<String>,
|
pub prev_custom_dir: Option<String>,
|
||||||
pub migrated: bool,
|
pub migrated: bool,
|
||||||
|
|
||||||
|
pub developer_mode: bool,
|
||||||
|
pub feature_flags: HashMap<FeatureFlag, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FeatureFlag {
|
||||||
|
PagePath,
|
||||||
|
ProjectBackground,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
@ -48,7 +60,7 @@ impl Settings {
|
|||||||
json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,
|
json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,
|
||||||
mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,
|
mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,
|
||||||
hook_pre_launch, hook_wrapper, hook_post_exit,
|
hook_pre_launch, hook_wrapper, hook_post_exit,
|
||||||
custom_dir, prev_custom_dir, migrated
|
custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar
|
||||||
FROM settings
|
FROM settings
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
@ -63,6 +75,7 @@ impl Settings {
|
|||||||
collapsed_navigation: res.collapsed_navigation == 1,
|
collapsed_navigation: res.collapsed_navigation == 1,
|
||||||
advanced_rendering: res.advanced_rendering == 1,
|
advanced_rendering: res.advanced_rendering == 1,
|
||||||
native_decorations: res.native_decorations == 1,
|
native_decorations: res.native_decorations == 1,
|
||||||
|
toggle_sidebar: res.toggle_sidebar == 1,
|
||||||
telemetry: res.telemetry == 1,
|
telemetry: res.telemetry == 1,
|
||||||
discord_rpc: res.discord_rpc == 1,
|
discord_rpc: res.discord_rpc == 1,
|
||||||
developer_mode: res.developer_mode == 1,
|
developer_mode: res.developer_mode == 1,
|
||||||
@ -95,6 +108,11 @@ impl Settings {
|
|||||||
custom_dir: res.custom_dir,
|
custom_dir: res.custom_dir,
|
||||||
prev_custom_dir: res.prev_custom_dir,
|
prev_custom_dir: res.prev_custom_dir,
|
||||||
migrated: res.migrated == 1,
|
migrated: res.migrated == 1,
|
||||||
|
feature_flags: res
|
||||||
|
.feature_flags
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|x| serde_json::from_str(x).ok())
|
||||||
|
.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +126,7 @@ impl Settings {
|
|||||||
let default_page = self.default_page.as_str();
|
let default_page = self.default_page.as_str();
|
||||||
let extra_launch_args = serde_json::to_string(&self.extra_launch_args)?;
|
let extra_launch_args = serde_json::to_string(&self.extra_launch_args)?;
|
||||||
let custom_env_vars = serde_json::to_string(&self.custom_env_vars)?;
|
let custom_env_vars = serde_json::to_string(&self.custom_env_vars)?;
|
||||||
|
let feature_flags = serde_json::to_string(&self.feature_flags)?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
@ -143,7 +162,10 @@ impl Settings {
|
|||||||
|
|
||||||
custom_dir = $23,
|
custom_dir = $23,
|
||||||
prev_custom_dir = $24,
|
prev_custom_dir = $24,
|
||||||
migrated = $25
|
migrated = $25,
|
||||||
|
|
||||||
|
toggle_sidebar = $26,
|
||||||
|
feature_flags = $27
|
||||||
",
|
",
|
||||||
max_concurrent_writes,
|
max_concurrent_writes,
|
||||||
max_concurrent_downloads,
|
max_concurrent_downloads,
|
||||||
@ -169,7 +191,9 @@ impl Settings {
|
|||||||
self.hooks.post_exit,
|
self.hooks.post_exit,
|
||||||
self.custom_dir,
|
self.custom_dir,
|
||||||
self.prev_custom_dir,
|
self.prev_custom_dir,
|
||||||
self.migrated
|
self.migrated,
|
||||||
|
self.toggle_sidebar,
|
||||||
|
feature_flags
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@ -185,6 +209,7 @@ pub enum Theme {
|
|||||||
Dark,
|
Dark,
|
||||||
Light,
|
Light,
|
||||||
Oled,
|
Oled,
|
||||||
|
System,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
@ -193,6 +218,7 @@ impl Theme {
|
|||||||
Theme::Dark => "dark",
|
Theme::Dark => "dark",
|
||||||
Theme::Light => "light",
|
Theme::Light => "light",
|
||||||
Theme::Oled => "oled",
|
Theme::Oled => "oled",
|
||||||
|
Theme::System => "system",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,6 +227,7 @@ impl Theme {
|
|||||||
"dark" => Theme::Dark,
|
"dark" => Theme::Dark,
|
||||||
"light" => Theme::Light,
|
"light" => Theme::Light,
|
||||||
"oled" => Theme::Oled,
|
"oled" => Theme::Oled,
|
||||||
|
"system" => Theme::System,
|
||||||
_ => Theme::Dark,
|
_ => Theme::Dark,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
ref="dropdown"
|
ref="dropdown"
|
||||||
no-auto-focus
|
no-auto-focus
|
||||||
:aria-id="dropdownId || null"
|
:aria-id="dropdownId || null"
|
||||||
|
placement="bottom-end"
|
||||||
@apply-hide="focusTrigger"
|
@apply-hide="focusTrigger"
|
||||||
@apply-show="focusMenuChild"
|
@apply-show="focusMenuChild"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import Checkbox from '../base/Checkbox.vue'
|
|||||||
import ContentListItem from './ContentListItem.vue'
|
import ContentListItem from './ContentListItem.vue'
|
||||||
import type { ContentItem } from './ContentListItem.vue'
|
import type { ContentItem } from './ContentListItem.vue'
|
||||||
import { DropdownIcon } from '@modrinth/assets'
|
import { DropdownIcon } from '@modrinth/assets'
|
||||||
import { createVirtualScroller } from 'vue-typed-virtual-list'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -13,12 +12,11 @@ const props = withDefaults(
|
|||||||
sortColumn: string
|
sortColumn: string
|
||||||
sortAscending: boolean
|
sortAscending: boolean
|
||||||
updateSort: (column: string) => void
|
updateSort: (column: string) => void
|
||||||
|
currentPage: number
|
||||||
}>(),
|
}>(),
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
const VirtualScroller = createVirtualScroller()
|
|
||||||
|
|
||||||
const selectionStates: Ref<Record<string, boolean>> = ref({})
|
const selectionStates: Ref<Record<string, boolean>> = ref({})
|
||||||
const selected: Ref<string[]> = computed(() =>
|
const selected: Ref<string[]> = computed(() =>
|
||||||
Object.keys(selectionStates.value).filter(
|
Object.keys(selectionStates.value).filter(
|
||||||
@ -42,6 +40,10 @@ function setSelected(value: boolean) {
|
|||||||
}
|
}
|
||||||
updateSelection()
|
updateSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paginatedItems = computed(() =>
|
||||||
|
props.items.slice((props.currentPage - 1) * 20, props.currentPage * 20),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -78,21 +80,19 @@ function setSelected(value: boolean) {
|
|||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-bg-raised rounded-xl">
|
<div class="bg-bg-raised rounded-xl">
|
||||||
<VirtualScroller :items="items" :default-size="64" style="height: 100%">
|
<ContentListItem
|
||||||
<template #item="{ ref, index }">
|
v-for="(itemRef, index) in paginatedItems"
|
||||||
<ContentListItem
|
:key="itemRef.filename"
|
||||||
v-model="selectionStates[ref.filename]"
|
v-model="selectionStates[itemRef.filename]"
|
||||||
:item="ref"
|
:item="itemRef"
|
||||||
:last="index === items.length - 1"
|
:last="index === paginatedItems.length - 1"
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
@update:model-value="updateSelection"
|
@update:model-value="updateSelection"
|
||||||
>
|
>
|
||||||
<template #actions="{ item }">
|
<template #actions="{ item }">
|
||||||
<slot name="actions" :item="item" />
|
<slot name="actions" :item="item" />
|
||||||
</template>
|
|
||||||
</ContentListItem>
|
|
||||||
</template>
|
</template>
|
||||||
</VirtualScroller>
|
</ContentListItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -26,14 +26,15 @@
|
|||||||
v-tooltip="
|
v-tooltip="
|
||||||
`${formatNumber(project.followers, false)} follower${project.downloads !== 1 ? 's' : ''}`
|
`${formatNumber(project.followers, false)} follower${project.downloads !== 1 ? 's' : ''}`
|
||||||
"
|
"
|
||||||
class="flex items-center gap-2 border-0 border-solid border-divider pr-4 md:border-r cursor-help"
|
class="flex items-center gap-2 border-0 border-solid border-divider pr-4 cursor-help"
|
||||||
|
:class="{ 'md:border-r': project.categories.length > 0 }"
|
||||||
>
|
>
|
||||||
<HeartIcon class="h-6 w-6 text-secondary" />
|
<HeartIcon class="h-6 w-6 text-secondary" />
|
||||||
<span class="font-semibold">
|
<span class="font-semibold">
|
||||||
{{ formatNumber(project.followers) }}
|
{{ formatNumber(project.followers) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden items-center gap-2 md:flex">
|
<div v-if="project.categories.length > 0" class="hidden items-center gap-2 md:flex">
|
||||||
<TagsIcon class="h-6 w-6 text-secondary" />
|
<TagsIcon class="h-6 w-6 text-secondary" />
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<TagItem v-for="(category, index) in project.categories" :key="index">
|
<TagItem v-for="(category, index) in project.categories" :key="index">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user