You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit '81ec068747a39e927c42273011252daaa58f1e14' into feature-clean
This commit is contained in:
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -11,4 +11,4 @@ contact_links:
|
||||
url: https://roadmap.modrinth.com
|
||||
- name: 📚 Documentation
|
||||
about: Useful documentation about Modrinth's API
|
||||
url: https://docs.modrinth.com
|
||||
url: https://docs.modrinth.com
|
||||
|
||||
4
.idea/daedalus.iml → .idea/code.iml
generated
4
.idea/daedalus.iml → .idea/code.iml
generated
@@ -3,8 +3,6 @@
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/daedalus_client_new/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/apps/daedalus_client/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/daedalus/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/apps/app-playground/src" isTestSource="false" />
|
||||
@@ -17,4 +15,4 @@
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
4
.idea/modules.xml
generated
4
.idea/modules.xml
generated
@@ -2,7 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/daedalus.iml" filepath="$PROJECT_DIR$/.idea/daedalus.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,12 +1,7 @@
|
||||
{
|
||||
"prettier.endOfLine": "lf",
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
|
||||
851
Cargo.lock
generated
851
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,4 +21,4 @@ strip = true # Remove debug symbols
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "27fb16b" }
|
||||
wry = { git = "https://github.com/modrinth/wry", rev ="cdbf938" }
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.8.906",
|
||||
"version": "0.9.201",
|
||||
"development_build": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -9,19 +9,20 @@
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"tsc:check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write ."
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@tauri-apps/api": "^2.0.0-rc.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.0.0-rc.0",
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-os": "^2.2.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.3.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
@@ -29,13 +30,14 @@
|
||||
"pinia": "^2.1.7",
|
||||
"posthog-js": "^1.158.2",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue": "^3.5.13",
|
||||
"vue-multiselect": "3.0.0",
|
||||
"vue-router": "4.3.0",
|
||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@nuxt/eslint-config": "^0.5.6",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
@@ -48,8 +50,9 @@
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.2.8",
|
||||
"vite": "^5.4.6",
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.4.0"
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
"web-types": "../../web-types.json"
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
|
||||
import { computed, ref, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
LogInIcon,
|
||||
HomeIcon,
|
||||
SearchIcon,
|
||||
LibraryIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
XIcon,
|
||||
// DownloadIcon,
|
||||
DownloadIcon,
|
||||
CompassIcon,
|
||||
MinimizeIcon,
|
||||
MaximizeIcon,
|
||||
RestoreIcon,
|
||||
LogOutIcon,
|
||||
RightArrowIcon,
|
||||
LeftArrowIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, Notifications } from '@modrinth/ui'
|
||||
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
// import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { get } from '@/helpers/settings'
|
||||
@@ -19,10 +28,9 @@ import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
||||
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
|
||||
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { isDev, getOS } from '@/helpers/utils.js'
|
||||
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
@@ -37,15 +45,26 @@ import IncompatibilityWarningModal from '@/components/ui/install_flow/Incompatib
|
||||
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
||||
import { useInstall } from '@/store/install.js'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
// import { check } from '@tauri-apps/plugin-updater'
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { get as getCreds, logout, login } from '@/helpers/mr_auth.js'
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||
import dayjs from 'dayjs'
|
||||
// import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
// import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const news = ref([])
|
||||
|
||||
const urlModal = ref(null)
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
@@ -65,8 +84,18 @@ const stateInitialized = ref(false)
|
||||
|
||||
const criticalErrorMessage = ref()
|
||||
|
||||
const isMaximized = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await useCheckDisableMouseover()
|
||||
|
||||
document.querySelector('body').addEventListener('click', handleClick)
|
||||
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelector('body').removeEventListener('click', handleClick)
|
||||
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
|
||||
})
|
||||
|
||||
async function setupApp() {
|
||||
@@ -75,14 +104,18 @@ async function setupApp() {
|
||||
native_decorations,
|
||||
theme,
|
||||
telemetry,
|
||||
personalized_ads,
|
||||
collapsed_navigation,
|
||||
advanced_rendering,
|
||||
onboarded,
|
||||
default_page,
|
||||
toggle_sidebar,
|
||||
developer_mode,
|
||||
feature_flags,
|
||||
} = await get()
|
||||
|
||||
if (default_page && default_page !== 'Home') {
|
||||
await router.push({ name: default_page })
|
||||
if (default_page === 'Library') {
|
||||
await router.push('/library')
|
||||
}
|
||||
|
||||
os.value = await getOS()
|
||||
@@ -96,6 +129,15 @@ async function setupApp() {
|
||||
themeStore.setThemeState(theme)
|
||||
themeStore.collapsedNavigation = collapsed_navigation
|
||||
themeStore.advancedRendering = advanced_rendering
|
||||
themeStore.toggleSidebar = toggle_sidebar
|
||||
themeStore.devMode = developer_mode
|
||||
themeStore.featureFlags = feature_flags
|
||||
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
|
||||
await getCurrentWindow().onResized(async () => {
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
})
|
||||
|
||||
initAnalytics()
|
||||
if (!telemetry) {
|
||||
@@ -132,8 +174,15 @@ 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)
|
||||
// checkUpdates()
|
||||
fetchCredentials()
|
||||
}
|
||||
|
||||
const stateFailed = ref(false)
|
||||
@@ -161,7 +210,6 @@ router.afterEach((to, from, failure) => {
|
||||
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
|
||||
})
|
||||
const route = useRoute()
|
||||
const isOnBrowse = computed(() => route.path.startsWith('/browse'))
|
||||
|
||||
const loading = useLoading()
|
||||
loading.setEnabled(false)
|
||||
@@ -177,6 +225,58 @@ const modInstallModal = ref()
|
||||
const installConfirmModal = ref()
|
||||
const incompatibilityWarningModal = ref()
|
||||
|
||||
const credentials = ref()
|
||||
|
||||
async function fetchCredentials() {
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (creds && creds.user_id) {
|
||||
creds.user = await get_user(creds.user_id).catch(handleError)
|
||||
}
|
||||
credentials.value = creds
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
await login().catch(handleError)
|
||||
await fetchCredentials()
|
||||
}
|
||||
|
||||
async function logOut() {
|
||||
await logout().catch(handleError)
|
||||
await fetchCredentials()
|
||||
}
|
||||
|
||||
const MIDAS_BITFLAG = 1 << 0
|
||||
const hasPlus = computed(
|
||||
() =>
|
||||
credentials.value &&
|
||||
credentials.value.user &&
|
||||
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
|
||||
)
|
||||
|
||||
const sidebarToggled = ref(true)
|
||||
|
||||
themeStore.$subscribe(() => {
|
||||
sidebarToggled.value = !themeStore.toggleSidebar
|
||||
})
|
||||
|
||||
const forceSidebar = computed(
|
||||
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
|
||||
)
|
||||
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
||||
// const showAd = computed(() => !(!sidebarVisible.value || hasPlus.value))
|
||||
|
||||
// watch(
|
||||
// showAd,
|
||||
// () => {
|
||||
// if (!showAd.value) {
|
||||
// hide_ads_window(true)
|
||||
// } else {
|
||||
// init_ads_window(true)
|
||||
// }
|
||||
// },
|
||||
// { immediate: true },
|
||||
// )
|
||||
|
||||
onMounted(() => {
|
||||
invoke('show_window')
|
||||
|
||||
@@ -189,41 +289,6 @@ onMounted(() => {
|
||||
install.setModInstallModal(modInstallModal)
|
||||
})
|
||||
|
||||
document.querySelector('body').addEventListener('click', function (e) {
|
||||
let target = e.target
|
||||
while (target != null) {
|
||||
if (target.matches('a')) {
|
||||
if (
|
||||
target.href &&
|
||||
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
|
||||
!target.classList.contains('router-link-active') &&
|
||||
!target.href.startsWith('http://localhost') &&
|
||||
!target.href.startsWith('https://tauri.localhost') &&
|
||||
!target.href.startsWith('http://tauri.localhost')
|
||||
) {
|
||||
open(target.href)
|
||||
}
|
||||
e.preventDefault()
|
||||
break
|
||||
}
|
||||
target = target.parentElement
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelector('body').addEventListener('auxclick', function (e) {
|
||||
// disables middle click -> new tab
|
||||
if (e.button === 1) {
|
||||
e.preventDefault()
|
||||
// instead do a left click
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
e.target.dispatchEvent(event)
|
||||
}
|
||||
})
|
||||
|
||||
const accounts = ref(null)
|
||||
|
||||
command_listener(handleCommand)
|
||||
@@ -247,7 +312,6 @@ async function handleCommand(e) {
|
||||
// const updateAvailable = ref(false)
|
||||
// async function checkUpdates() {
|
||||
// const update = await check()
|
||||
// console.log(update)
|
||||
// updateAvailable.value = !!update
|
||||
|
||||
// setTimeout(
|
||||
@@ -257,76 +321,159 @@ async function handleCommand(e) {
|
||||
// 5 * 1000 * 60,
|
||||
// )
|
||||
// }
|
||||
|
||||
function handleClick(e) {
|
||||
let target = e.target
|
||||
while (target != null) {
|
||||
if (target.matches('a')) {
|
||||
if (
|
||||
target.href &&
|
||||
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
|
||||
!target.classList.contains('router-link-active') &&
|
||||
!target.href.startsWith('http://localhost') &&
|
||||
!target.href.startsWith('https://tauri.localhost') &&
|
||||
!target.href.startsWith('http://tauri.localhost')
|
||||
) {
|
||||
openUrl(target.href)
|
||||
}
|
||||
e.preventDefault()
|
||||
break
|
||||
}
|
||||
target = target.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
function handleAuxClick(e) {
|
||||
// disables middle click -> new tab
|
||||
if (e.button === 1) {
|
||||
e.preventDefault()
|
||||
// instead do a left click
|
||||
const event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
e.target.dispatchEvent(event)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
|
||||
<div v-if="stateInitialized" class="app-container">
|
||||
<div class="nav-container">
|
||||
<div class="nav-section">
|
||||
<suspense>
|
||||
<AccountsCard ref="accounts" mode="small" />
|
||||
</suspense>
|
||||
<div class="pages-list">
|
||||
<RouterLink v-tooltip="'Home'" to="/" class="btn icon-only collapsed-button">
|
||||
<HomeIcon />
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-tooltip="'Browse'"
|
||||
to="/browse/modpack"
|
||||
class="btn icon-only collapsed-button"
|
||||
:class="{
|
||||
'router-link-active': isOnBrowse,
|
||||
}"
|
||||
>
|
||||
<SearchIcon />
|
||||
</RouterLink>
|
||||
<RouterLink v-tooltip="'Library'" to="/library" class="btn icon-only collapsed-button">
|
||||
<LibraryIcon />
|
||||
</RouterLink>
|
||||
<Suspense>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings pages-list">
|
||||
<!-- <button
|
||||
v-if="updateAvailable"
|
||||
v-tooltip="'Install update'"
|
||||
class="btn btn-outline btn-primary icon-only collapsed-button"
|
||||
@click="restartApp()"
|
||||
<div id="teleports"></div>
|
||||
<div v-if="stateInitialized" class="app-grid-layout relative">
|
||||
<Suspense>
|
||||
<AppSettingsModal ref="settingsModal" />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</Suspense>
|
||||
<div
|
||||
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
|
||||
>
|
||||
<NavButton v-tooltip.right="'Home'" to="/">
|
||||
<HomeIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Discover content'"
|
||||
to="/browse/modpack"
|
||||
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
|
||||
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
|
||||
>
|
||||
<CompassIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Library'"
|
||||
to="/library"
|
||||
:is-subpage="
|
||||
() =>
|
||||
route.path.startsWith('/instance') ||
|
||||
((route.path.startsWith('/browse') || route.path.startsWith('/project')) &&
|
||||
route.query.i)
|
||||
"
|
||||
>
|
||||
<LibraryIcon />
|
||||
</NavButton>
|
||||
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||
<suspense>
|
||||
<QuickInstanceSwitcher />
|
||||
</suspense>
|
||||
<NavButton
|
||||
v-tooltip.right="'Create new instance'"
|
||||
:to="() => $refs.installationModal.show()"
|
||||
:disabled="offline"
|
||||
>
|
||||
<PlusIcon />
|
||||
</NavButton>
|
||||
<div class="flex flex-grow"></div>
|
||||
<NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
|
||||
<DownloadIcon />
|
||||
</NavButton>
|
||||
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||
<SettingsIcon />
|
||||
</NavButton>
|
||||
<ButtonStyled v-if="credentials" type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'sign-out',
|
||||
action: () => logOut(),
|
||||
color: 'danger',
|
||||
},
|
||||
]"
|
||||
direction="left"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button> -->
|
||||
<Button
|
||||
v-tooltip="'Create profile'"
|
||||
class="sleek-primary collapsed-button"
|
||||
icon-only
|
||||
:disabled="offline"
|
||||
@click="() => $refs.installationModal.show()"
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<RouterLink v-tooltip="'Settings'" to="/settings" class="btn icon-only collapsed-button">
|
||||
<SettingsIcon />
|
||||
</RouterLink>
|
||||
</div>
|
||||
<Avatar
|
||||
:src="credentials.user.avatar_url"
|
||||
:alt="credentials.user.username"
|
||||
size="32px"
|
||||
circle
|
||||
/>
|
||||
<template #sign-out> <LogOutIcon /> Sign out </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<NavButton v-else v-tooltip.right="'Sign in'" :to="() => signIn()">
|
||||
<LogInIcon />
|
||||
<template #label>Sign in</template>
|
||||
</NavButton>
|
||||
</div>
|
||||
<div class="view">
|
||||
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
|
||||
<h1>{{ criticalErrorMessage.header }}</h1>
|
||||
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
|
||||
<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-3">
|
||||
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
|
||||
<div class="flex items-center gap-1 ml-3">
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</div>
|
||||
<Breadcrumbs class="pt-[2px]" />
|
||||
</div>
|
||||
<div class="appbar-row">
|
||||
<div data-tauri-drag-region class="appbar">
|
||||
<section class="navigation-controls">
|
||||
<Breadcrumbs data-tauri-drag-region />
|
||||
</section>
|
||||
<section class="mod-stats">
|
||||
<Suspense>
|
||||
<RunningAppBar />
|
||||
</Suspense>
|
||||
</section>
|
||||
<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">
|
||||
<Suspense>
|
||||
<RunningAppBar />
|
||||
</Suspense>
|
||||
</div>
|
||||
<section v-if="!nativeDecorations" class="window-controls">
|
||||
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
|
||||
@@ -337,30 +484,125 @@ async function handleCommand(e) {
|
||||
icon-only
|
||||
@click="() => getCurrentWindow().toggleMaximize()"
|
||||
>
|
||||
<MaximizeIcon />
|
||||
<RestoreIcon v-if="isMaximized" />
|
||||
<MaximizeIcon v-else />
|
||||
</Button>
|
||||
<Button class="titlebar-button close" icon-only @click="handleClose">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="stateInitialized"
|
||||
class="app-contents experimental-styles-within"
|
||||
:class="{ 'sidebar-enabled': sidebarVisible }"
|
||||
>
|
||||
<div class="app-viewport flex-grow router-view">
|
||||
<div
|
||||
class="loading-indicator-container h-8 fixed z-50"
|
||||
:style="{
|
||||
top: 'calc(var(--top-bar-height))',
|
||||
left: 'calc(var(--left-bar-width))',
|
||||
width: 'calc(100% - var(--left-bar-width) - var(--right-bar-width))',
|
||||
}"
|
||||
>
|
||||
<ModrinthLoadingIndicator />
|
||||
</div>
|
||||
<div class="router-view">
|
||||
<ModrinthLoadingIndicator
|
||||
offset-height="var(--appbar-height)"
|
||||
offset-width="var(--sidebar-width)"
|
||||
/>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
|
||||
<component :is="Component"></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
{{ route.fullPath }}
|
||||
</div>
|
||||
<div
|
||||
id="background-teleport-target"
|
||||
class="absolute h-full -z-10 rounded-tl-[--radius-xl] overflow-hidden"
|
||||
:style="{
|
||||
width: 'calc(100% - var(--right-bar-width))',
|
||||
}"
|
||||
></div>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
|
||||
<component :is="Component"></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
<div
|
||||
class="app-sidebar mt-px shrink-0 flex flex-col border-0 border-l-[1px] border-[--brand-gradient-border] border-solid overflow-auto"
|
||||
:class="{ 'has-plus': hasPlus }"
|
||||
>
|
||||
<div
|
||||
class="app-sidebar-scrollable flex-grow shrink overflow-y-auto relative"
|
||||
:class="{ 'pb-12': !hasPlus }"
|
||||
>
|
||||
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
|
||||
<div class="sidebar-default-content" :class="{ 'sidebar-enabled': sidebarVisible }">
|
||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<h3 class="text-lg m-0">Playing as</h3>
|
||||
<suspense>
|
||||
<AccountsCard ref="accounts" mode="small" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<suspense>
|
||||
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div v-if="news && news.length > 0" class="pt-4 flex flex-col">
|
||||
<h3 class="px-4 text-lg m-0">News</h3>
|
||||
<template v-for="(item, index) in news" :key="`news-${index}`">
|
||||
<a
|
||||
:class="`flex flex-col outline-offset-[-4px] hover:bg-[--brand-gradient-border] focus:bg-[--brand-gradient-border] px-4 transition-colors ${index === 0 ? 'pt-2 pb-4' : 'py-4'}`"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
rel="external"
|
||||
>
|
||||
<img
|
||||
:src="item.thumbnail"
|
||||
alt="News thumbnail"
|
||||
aria-hidden="true"
|
||||
class="w-full aspect-[3/1] object-cover rounded-2xl border-[1px] border-solid border-[--brand-gradient-border]"
|
||||
/>
|
||||
<h4 class="mt-2 mb-0 text-sm leading-none text-contrast font-semibold">
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
|
||||
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
|
||||
{{ dayjs(item.date).fromNow() }}
|
||||
</p>
|
||||
</a>
|
||||
<hr
|
||||
v-if="index !== news.length - 1"
|
||||
class="h-px my-[-2px] mx-4 border-0 m-0 bg-[--brand-gradient-border]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <template v-if="showAd">
|
||||
<a
|
||||
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"
|
||||
target="_blank"
|
||||
>
|
||||
<ArrowBigUpDashIcon class="text-2xl" /> Upgrade to Modrinth+
|
||||
</a>
|
||||
<PromotionWrapper />
|
||||
</template> -->
|
||||
</div>
|
||||
<div class="view">
|
||||
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
|
||||
<h1>{{ criticalErrorMessage.header }}</h1>
|
||||
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<URLConfirmModal ref="urlModal" />
|
||||
<Notifications ref="notificationsWrapper" />
|
||||
<Notifications ref="notificationsWrapper" sidebar />
|
||||
<ErrorModal ref="errorModal" />
|
||||
<ModInstallModal ref="modInstallModal" />
|
||||
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
||||
@@ -368,21 +610,6 @@ async function handleCommand(e) {
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
z-index: 20;
|
||||
display: none;
|
||||
@@ -395,168 +622,188 @@ async function handleCommand(e) {
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all ease-in-out 0.1s;
|
||||
background-color: var(--color-raised-bg);
|
||||
background-color: transparent;
|
||||
color: var(--color-base);
|
||||
border-radius: 0;
|
||||
height: 3.25rem;
|
||||
height: 100%;
|
||||
width: 3rem;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0.75rem;
|
||||
width: 3.75rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
border-radius: 999999px;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
aspect-ratio: 1 / 1;
|
||||
margin-block: auto;
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
scale: 0.9;
|
||||
transition: all ease-in-out 0.2s;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&.close {
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--color-red);
|
||||
color: var(--color-accent-contrast);
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-contrast);
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-button-bg);
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-container {
|
||||
--appbar-height: 3.25rem;
|
||||
--sidebar-width: 4.5rem;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
.app-grid-layout,
|
||||
.app-contents {
|
||||
--top-bar-height: 3rem;
|
||||
--left-bar-width: 4rem;
|
||||
--right-bar-width: 300px;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
.app-grid-layout {
|
||||
display: grid;
|
||||
grid-template: 'status status' 'nav dummy';
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
position: relative;
|
||||
//z-index: 0;
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
|
||||
padding: var(--gap-md);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.pages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
.app-grid-navbar {
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
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;
|
||||
.app-grid-statusbar {
|
||||
grid-area: status;
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
color: var(--color-contrast);
|
||||
background: var(--color-button-bg);
|
||||
box-shadow: var(--shadow-floating);
|
||||
}
|
||||
.app-contents {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: var(--left-bar-width);
|
||||
top: var(--top-bar-height);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: calc(100vh - var(--top-bar-height));
|
||||
background-color: var(--color-bg);
|
||||
border-top-left-radius: var(--radius-xl);
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0px;
|
||||
// transition: grid-template-columns 0.4s ease-in-out;
|
||||
|
||||
&.primary {
|
||||
color: var(--color-accent-contrast);
|
||||
background-color: var(--color-brand);
|
||||
&.sidebar-enabled {
|
||||
grid-template-columns: 1fr 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.loading-indicator-container {
|
||||
border-top-left-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
.app-sidebar {
|
||||
overflow: visible;
|
||||
width: 300px;
|
||||
position: relative;
|
||||
height: calc(100vh - var(--top-bar-height));
|
||||
background: var(--brand-gradient-bg);
|
||||
|
||||
--color-button-bg: var(--brand-gradient-button);
|
||||
--color-button-bg-hover: var(--brand-gradient-border);
|
||||
--color-divider: var(--brand-gradient-border);
|
||||
--color-divider-dark: var(--brand-gradient-border);
|
||||
}
|
||||
|
||||
.app-sidebar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 250px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 5rem;
|
||||
background: var(--brand-gradient-fade-out-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-sidebar.has-plus::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar::before {
|
||||
content: '';
|
||||
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.2) inset;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -2rem;
|
||||
width: 2rem;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-viewport {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap-md);
|
||||
.app-contents::before {
|
||||
z-index: 1;
|
||||
content: '';
|
||||
position: fixed;
|
||||
left: var(--left-bar-width);
|
||||
top: var(--top-bar-height);
|
||||
right: calc(-1 * var(--left-bar-width));
|
||||
bottom: calc(-1 * var(--left-bar-width));
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow:
|
||||
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
|
||||
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
padding: var(--gap-sm) 0;
|
||||
}
|
||||
.sidebar-teleport-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sidebar-default-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-teleport-content:empty + .sidebar-default-content.sidebar-enabled {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.mac {
|
||||
.nav-container {
|
||||
padding-top: calc(var(--gap-md) + 1.75rem);
|
||||
}
|
||||
|
||||
.account-card,
|
||||
.card-section {
|
||||
top: calc(var(--gap-md) + 1.75rem);
|
||||
.app-grid-statusbar {
|
||||
padding-left: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,3 +825,4 @@ async function handleCommand(e) {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
export { default as ServerIcon } from './server.svg'
|
||||
export { default as MinimizeIcon } from './minimize.svg'
|
||||
export { default as MaximizeIcon } from './maximize.svg'
|
||||
export { default as SwapIcon } from './arrow-left-right.svg'
|
||||
export { default as ToggleIcon } from './toggle.svg'
|
||||
export { default as PackageIcon } from './package.svg'
|
||||
export { default as VersionIcon } from './milestone.svg'
|
||||
export { default as MoreIcon } from './more.svg'
|
||||
export { default as TextInputIcon } from './text-cursor-input.svg'
|
||||
export { default as AddProjectImage } from './add-project.svg'
|
||||
export { default as NewInstanceImage } from './new-instance.svg'
|
||||
export { default as MenuIcon } from './menu.svg'
|
||||
export { default as BugIcon } from './bug.svg'
|
||||
export { default as ChatIcon } from './messages-square.svg'
|
||||
export { default as Pirate } from './pirate.svg'
|
||||
export { default as Microsoft } from './microsoft.svg'
|
||||
|
||||
28
apps/app-frontend/src/assets/modrinth_app.svg
Normal file
28
apps/app-frontend/src/assets/modrinth_app.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
24
apps/app-frontend/src/assets/modrinth_servers.svg
Normal file
24
apps/app-frontend/src/assets/modrinth_servers.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" version="1.1" viewBox="0 0 1793 199">
|
||||
<g>
|
||||
<g id="Layer_1">
|
||||
<g id="green" fill="var(--color-brand)">
|
||||
<path d="M1184.1,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
||||
<path d="M1291.1,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
||||
<path d="M1357.2,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
||||
<path d="M1460,165.3l-40.8-95.1h23.2l35.1,83.9h-11.4l36.3-83.9h21.4l-40.8,95.1h-23Z"/>
|
||||
<path d="M1579.6,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
||||
<path d="M1645.7,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
||||
<path d="M1749.9,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
||||
<g>
|
||||
<path d="M9.8,143l63.4-38.1-5.8-15.3,18.1-18.6,22.9-4.9,6.6,8.2-10.6,10.7-9.2,2.9-6.6,6.8,3.2,9,6.5,6.9,9.2-2.5,6.6-7.2,14.3-4.5,4.3,9.6-14.8,18.1-24.8,7.8-11.1-12.4-63.6,38.2c-3-3.9-6.5-9.4-8.8-14.7ZM192.8,65.4l-50.4,13.6c2.8,7.4,3.7,11.7,4.5,16.5l50.3-13.6c-.8-5.4-2.2-10.8-4.4-16.5Z" fill-rule="evenodd"/>
|
||||
<path d="M17.3,106.5c3.6,42.1,38.9,75.2,82,75.2s60.7-18.9,74-46.3l16.4,5.7c-15.8,34.1-50.3,57.9-90.4,57.9S3.6,158.2,0,106.5h17.3ZM.3,89.4C5.3,39.2,47.8,0,99.3,0s99.5,44.6,99.5,99.5-1.1,17.4-3.3,25.5l-16.3-5.7c1.6-6.5,2.4-13.1,2.4-19.8,0-45.4-36.9-82.3-82.3-82.3S22.6,48.7,17.6,89.4H.3Z" fill-rule="evenodd"/>
|
||||
<path d="M99,51.6c-26.4,0-47.9,21.5-47.9,48s21.5,48,48,48,2.7,0,4-.2l4.8,16.8c-2.9.4-5.8.6-8.8.6-36,0-65.2-29.2-65.2-65.2S63.1,34.4,99,34.4s1.8,0,2.7,0l-2.7,17.1ZM118.6,37.4c26.4,8.3,45.6,33,45.6,62.2s-16.4,50.2-39.8,60l-4.8-16.7c16.2-7.7,27.4-24.2,27.4-43.3s-13-38.1-31.1-44.9l2.7-17.2Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="black" fill="currentColor">
|
||||
<path d="M354.8,69.2c12,0,21.7,3.4,28.6,10.4,7,7.2,10.6,17.5,10.6,31.5v54.8h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,5-6.8,12.2-6.8,21.3v48.5h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,4.8-6.8,12-6.8,21.3v48.5h-22.4v-95.6h21.3v12.2c3.6-4.3,8.1-7.5,13.4-9.8,5.4-2.3,11.3-3.4,17.9-3.4s13.6,1.3,19.2,3.9c5.5,2.9,9.8,6.8,13.1,12,3.9-5,8.9-8.9,15.2-11.8,6.3-2.7,13.1-4.1,20.6-4.1ZM466,167.2c-9.7,0-18.4-2.1-26.1-6.3-7.6-4-13.8-10.1-18.1-17.5-4.5-7.3-6.6-15.7-6.6-25.2s2.1-17.9,6.6-25.2c4.3-7.4,10.6-13.4,18.1-17.4,7.7-4.1,16.5-6.3,26.1-6.3s18.6,2.1,26.3,6.3c7.7,4.1,13.8,10,18.3,17.4,4.3,7.3,6.4,15.7,6.4,25.2s-2.1,17.9-6.4,25.2c-4.5,7.5-10.6,13.4-18.3,17.5-7.7,4.1-16.5,6.3-26.3,6.3h0ZM466,148c8.2,0,15-2.7,20.4-8.2,5.4-5.5,8.1-12.7,8.1-21.7s-2.7-16.1-8.1-21.7c-5.4-5.5-12.2-8.2-20.4-8.2s-15,2.7-20.2,8.2c-5.4,5.5-8.1,12.7-8.1,21.7s2.7,16.1,8.1,21.7c5.2,5.5,12,8.2,20.2,8.2ZM631.5,33.1v132.8h-21.5v-12.3c-3.7,4.4-8.3,7.9-13.6,10.2-5.5,2.3-11.5,3.4-18.1,3.4s-17.4-2-24.7-6.1c-7.3-4.1-13.2-9.8-17.4-17.4-4.1-7.3-6.3-15.9-6.3-25.6s2.1-18.3,6.3-25.6c4.1-7.3,10-13.1,17.4-17.2,7.3-4.1,15.6-6.1,24.7-6.1s12.2,1.1,17.4,3.2c5.2,2.1,9.8,5.4,13.4,9.7v-49h22.4ZM581.1,148c5.4,0,10.2-1.3,14.5-3.8,4.3-2.3,7.7-5.9,10.2-10.4,2.5-4.5,3.8-9.8,3.8-15.7s-1.3-11.3-3.8-15.7c-2.5-4.5-5.9-8.1-10.2-10.6-4.3-2.3-9.1-3.6-14.5-3.6s-10.2,1.3-14.5,3.6c-4.3,2.5-7.7,6.1-10.2,10.6-2.5,4.5-3.8,9.8-3.8,15.7s1.3,11.3,3.8,15.7c2.5,4.5,5.9,8.1,10.2,10.4,4.3,2.5,9.1,3.8,14.5,3.8ZM681.6,84.3c6.4-10,17.7-15,34-15v21.3c-1.7-.3-3.4-.5-5.2-.5-8.8,0-15.6,2.5-20.4,7.5-4.8,5.2-7.3,12.5-7.3,22v46.4h-22.4v-95.6h21.3v14h0ZM734.1,70.3h22.4v95.6h-22.4v-95.6ZM745.4,54.6c-4.1,0-7.5-1.3-10.2-3.9-2.7-2.4-4.2-5.9-4.1-9.5,0-3.8,1.4-7,4.1-9.7,2.7-2.5,6.1-3.8,10.2-3.8s7.5,1.3,10.2,3.6c2.7,2.5,4.1,5.5,4.1,9.3s-1.3,7.2-3.9,9.8c-2.7,2.7-6.3,4.1-10.4,4.1ZM839.5,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4v-95.6h21.3v12.3c3.8-4.5,8.4-7.7,14-10,5.5-2.3,12-3.4,19-3.4ZM964.8,160.7c-2.8,2.2-6,3.9-9.5,4.8-3.9,1.1-7.9,1.6-12,1.6-10.6,0-18.6-2.7-24.3-8.2-5.7-5.5-8.6-13.4-8.6-24v-46h-15.7v-17.9h15.7v-21.8h22.4v21.8h25.6v17.9h-25.6v45.5c0,4.7,1.1,8.2,3.4,10.6,2.3,2.5,5.5,3.8,9.8,3.8s9.1-1.3,12.5-3.9l6.3,15.9ZM1036.9,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4V33.1h22.4v48.3c3.8-3.9,8.2-7,13.8-9.1,5.4-2,11.5-3,18.1-3Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
BIN
apps/app-frontend/src/assets/sad-modrinth-bot.webp
Normal file
BIN
apps/app-frontend/src/assets/sad-modrinth-bot.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
@@ -80,19 +80,25 @@ input {
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: var(--gap-md);
|
||||
border: 3px solid var(--color-scrollbar);
|
||||
width: 16px;
|
||||
border: 3px solid transparent;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--color-bg);
|
||||
border: 3px solid var(--color-bg);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-scrollbar);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 3px solid var(--color-bg);
|
||||
border: 5px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
@@ -111,4 +117,8 @@ img {
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
@import '@modrinth/assets/omorphia.scss';
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, Card, DropdownSelect } from '@modrinth/ui'
|
||||
import { Button, DropdownSelect } from '@modrinth/ui'
|
||||
import { formatCategoryHeader } from '@modrinth/utils'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -120,12 +120,11 @@ const handleOptionsClick = async (args) => {
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
const group = ref('Category')
|
||||
const filters = ref('All profiles')
|
||||
const group = ref('Group')
|
||||
const sortBy = ref('Name')
|
||||
|
||||
const filteredResults = computed(() => {
|
||||
let instances = props.instances.filter((instance) => {
|
||||
const instances = props.instances.filter((instance) => {
|
||||
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
|
||||
@@ -159,16 +158,6 @@ const filteredResults = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
if (filters.value === 'Custom instances') {
|
||||
instances = instances.filter((instance) => {
|
||||
return !instance.linked_data
|
||||
})
|
||||
} else if (filters.value === 'Downloaded modpacks') {
|
||||
instances = instances.filter((instance) => {
|
||||
return instance.linked_data
|
||||
})
|
||||
}
|
||||
|
||||
const instanceMap = new Map()
|
||||
|
||||
if (group.value === 'Loader') {
|
||||
@@ -188,7 +177,7 @@ const filteredResults = computed(() => {
|
||||
|
||||
instanceMap.get(instance.game_version).push(instance)
|
||||
})
|
||||
} else if (group.value === 'Category') {
|
||||
} else if (group.value === 'Group') {
|
||||
instances.forEach((instance) => {
|
||||
if (instance.groups.length === 0) {
|
||||
instance.groups.push('None')
|
||||
@@ -229,53 +218,37 @@ const filteredResults = computed(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ConfirmModalWrapper
|
||||
ref="confirmModal"
|
||||
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="deleteProfile"
|
||||
/>
|
||||
<Card class="header">
|
||||
<div class="iconified-input">
|
||||
<div class="flex gap-2">
|
||||
<div class="iconified-input flex-1">
|
||||
<SearchIcon />
|
||||
<input v-model="search" type="text" placeholder="Search" class="search-input" />
|
||||
<input v-model="search" type="text" placeholder="Search" />
|
||||
<Button class="r-btn" @click="() => (search = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="labeled_button">
|
||||
<span>Sort by</span>
|
||||
<DropdownSelect
|
||||
v-model="sortBy"
|
||||
class="sort-dropdown"
|
||||
name="Sort Dropdown"
|
||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
||||
placeholder="Select..."
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_button">
|
||||
<span>Filter by</span>
|
||||
<DropdownSelect
|
||||
v-model="filters"
|
||||
class="filter-dropdown"
|
||||
name="Filter Dropdown"
|
||||
:options="['All profiles', 'Custom instances', 'Downloaded modpacks']"
|
||||
placeholder="Select..."
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_button">
|
||||
<span>Group by</span>
|
||||
<DropdownSelect
|
||||
v-model="group"
|
||||
class="group-dropdown"
|
||||
name="Group Dropdown"
|
||||
:options="['Category', 'Loader', 'Game version', 'None']"
|
||||
placeholder="Select..."
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="sortBy"
|
||||
name="Sort Dropdown"
|
||||
class="max-w-[16rem]"
|
||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
||||
placeholder="Select..."
|
||||
>
|
||||
<span class="font-semibold text-primary">Sort by: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</DropdownSelect>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="group"
|
||||
class="max-w-[16rem]"
|
||||
name="Group Dropdown"
|
||||
:options="['Group', 'Loader', 'Game version', 'None']"
|
||||
placeholder="Select..."
|
||||
>
|
||||
<span class="font-semibold text-primary">Group by: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
<div
|
||||
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
||||
key,
|
||||
@@ -298,6 +271,14 @@ const filteredResults = computed(() => {
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<ConfirmModalWrapper
|
||||
ref="confirmModal"
|
||||
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="deleteProfile"
|
||||
/>
|
||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
<template #stop> <StopCircleIcon /> Stop </template>
|
||||
@@ -315,7 +296,6 @@ const filteredResults = computed(() => {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
@@ -383,9 +363,9 @@ const filteredResults = computed(() => {
|
||||
|
||||
.instances {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
margin-right: auto;
|
||||
scroll-behavior: smooth;
|
||||
overflow-y: auto;
|
||||
|
||||
141
apps/app-frontend/src/components/LoadingIndicatorBar.vue
Normal file
141
apps/app-frontend/src/components/LoadingIndicatorBar.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useLoading } from '@/store/state.js'
|
||||
|
||||
const props = defineProps({
|
||||
throttle: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'var(--loading-bar-gradient)',
|
||||
},
|
||||
})
|
||||
|
||||
const indicator = useLoadingIndicator({
|
||||
duration: props.duration,
|
||||
throttle: props.throttle,
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => indicator.clear)
|
||||
|
||||
const loading = useLoading()
|
||||
|
||||
watch(loading, (newValue) => {
|
||||
if (newValue.barEnabled) {
|
||||
if (newValue.loading) {
|
||||
indicator.start()
|
||||
} else {
|
||||
indicator.finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function useLoadingIndicator(opts) {
|
||||
const progress = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const step = computed(() => 10000 / opts.duration)
|
||||
|
||||
let _timer = null
|
||||
let _throttle = null
|
||||
|
||||
function start() {
|
||||
clear()
|
||||
progress.value = 0
|
||||
if (opts.throttle) {
|
||||
_throttle = setTimeout(() => {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}, opts.throttle)
|
||||
} else {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
function finish() {
|
||||
progress.value = 100
|
||||
_hide()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
clearInterval(_timer)
|
||||
clearTimeout(_throttle)
|
||||
_timer = null
|
||||
_throttle = null
|
||||
}
|
||||
|
||||
function _increase(num) {
|
||||
progress.value = Math.min(100, progress.value + num)
|
||||
}
|
||||
|
||||
function _hide() {
|
||||
clear()
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 400)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function _startTimer() {
|
||||
_timer = setInterval(() => {
|
||||
_increase(step.value)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return { progress, isLoading, start, finish, clear }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="loading-indicator-bar"
|
||||
:style="{
|
||||
'--_width': `${indicator.progress.value}%`,
|
||||
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
||||
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
||||
top: `0`,
|
||||
right: `0`,
|
||||
left: `${props.offsetWidth}`,
|
||||
pointerEvents: 'none',
|
||||
width: `var(--_width)`,
|
||||
height: `var(--_height)`,
|
||||
borderRadius: `var(--_height)`,
|
||||
// opacity: `var(--_opacity)`,
|
||||
background: `${props.color}`,
|
||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
||||
zIndex: 6,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.loading-indicator-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: var(--_width);
|
||||
bottom: 0;
|
||||
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
||||
opacity: calc(var(--_opacity) * 0.1);
|
||||
z-index: 5;
|
||||
transition:
|
||||
width 0.1s ease-in-out,
|
||||
opacity 0.1s ease-out;
|
||||
}
|
||||
</style>
|
||||
@@ -25,6 +25,7 @@ import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -165,13 +166,7 @@ const handleOptionsClick = async (args) => {
|
||||
break
|
||||
}
|
||||
case 'open_link':
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: `https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
},
|
||||
})
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
await navigator.clipboard.writeText(
|
||||
@@ -181,6 +176,7 @@ const handleOptionsClick = async (args) => {
|
||||
}
|
||||
}
|
||||
|
||||
const maxInstancesPerCompactRow = ref(1)
|
||||
const maxInstancesPerRow = ref(1)
|
||||
const maxProjectsPerRow = ref(1)
|
||||
|
||||
@@ -190,17 +186,34 @@ const calculateCardsPerRow = () => {
|
||||
// Convert container width from pixels to rem
|
||||
const containerWidthInRem =
|
||||
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 1) / 11)
|
||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 1) / 19)
|
||||
|
||||
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||
|
||||
if (maxInstancesPerRow.value < 5) {
|
||||
maxInstancesPerRow.value *= 2
|
||||
}
|
||||
if (maxInstancesPerCompactRow.value < 5) {
|
||||
maxInstancesPerCompactRow.value *= 2
|
||||
}
|
||||
if (maxProjectsPerRow.value < 3) {
|
||||
maxProjectsPerRow.value *= 2
|
||||
}
|
||||
}
|
||||
|
||||
const rowContainer = ref(null)
|
||||
const resizeObserver = ref(null)
|
||||
onMounted(() => {
|
||||
calculateCardsPerRow()
|
||||
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
||||
resizeObserver.value.observe(rowContainer.value)
|
||||
window.addEventListener('resize', calculateCardsPerRow)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', calculateCardsPerRow)
|
||||
resizeObserver.value.unobserve(rowContainer.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -213,17 +226,33 @@ onUnmounted(() => {
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteProfile"
|
||||
/>
|
||||
<div class="content">
|
||||
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
||||
<div class="header">
|
||||
<router-link :to="row.route">{{ row.label }}</router-link>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
<section v-if="row.instance" ref="modsRow" class="instances">
|
||||
<div ref="rowContainer" class="flex flex-col gap-4">
|
||||
<div v-for="(row, rowIndex) in actualInstances" ref="rows" :key="row.label" class="row">
|
||||
<router-link
|
||||
class="flex mb-3 leading-none items-center gap-1 text-primary text-lg font-bold hover:underline group"
|
||||
:class="{ 'mt-1': rowIndex > 0 }"
|
||||
:to="row.route"
|
||||
>
|
||||
{{ row.label }}
|
||||
<ChevronRightIcon
|
||||
class="h-5 w-5 stroke-[3px] group-hover:translate-x-1 transition-transform group-hover:text-brand"
|
||||
/>
|
||||
</router-link>
|
||||
<section
|
||||
v-if="row.instance"
|
||||
ref="modsRow"
|
||||
class="instances"
|
||||
:class="{ compact: row.compact }"
|
||||
>
|
||||
<Instance
|
||||
v-for="instance in row.instances.slice(0, maxInstancesPerRow)"
|
||||
:key="(instance?.project_id || instance?.id) + instance.install_stage"
|
||||
v-for="(instance, instanceIndex) in row.instances.slice(
|
||||
0,
|
||||
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
||||
)"
|
||||
:key="row.label + instance.path"
|
||||
:instance="instance"
|
||||
:compact="row.compact"
|
||||
:first="instanceIndex === 0"
|
||||
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
||||
/>
|
||||
</section>
|
||||
@@ -260,7 +289,6 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
-ms-overflow-style: none;
|
||||
@@ -294,31 +322,36 @@ onUnmounted(() => {
|
||||
|
||||
a {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bolder;
|
||||
white-space: nowrap;
|
||||
color: var(--color-contrast);
|
||||
color: var(--color-base);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
color: var(--color-contrast);
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
color: var(--color-base);
|
||||
}
|
||||
}
|
||||
|
||||
.instances {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
grid-gap: 0.75rem;
|
||||
width: 100%;
|
||||
|
||||
&.compact {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.projects {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
grid-gap: 1rem;
|
||||
grid-gap: 0.75rem;
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useLoading } from '@/store/state.js'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
throttle: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
color: {
|
||||
type: [String, Boolean],
|
||||
default:
|
||||
'repeating-linear-gradient(to right, var(--color-brand) 0%, var(--color-brand) 100%)',
|
||||
},
|
||||
offsetWidth: {
|
||||
type: String,
|
||||
default: '208px',
|
||||
},
|
||||
offsetHeight: {
|
||||
type: String,
|
||||
default: '52px',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const indicator = useLoadingIndicator({
|
||||
duration: props.duration,
|
||||
throttle: props.throttle,
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => indicator.clear)
|
||||
|
||||
const loading = useLoading()
|
||||
|
||||
watch(loading, (newValue) => {
|
||||
if (newValue.barEnabled) {
|
||||
if (newValue.loading) {
|
||||
indicator.start()
|
||||
} else {
|
||||
indicator.finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
position: 'fixed',
|
||||
top: props.offsetHeight,
|
||||
right: 0,
|
||||
left: props.offsetWidth,
|
||||
pointerEvents: 'none',
|
||||
width: `calc((100vw - ${props.offsetWidth}) * ${indicator.progress.value / 100})`,
|
||||
height: `${props.height}px`,
|
||||
opacity: indicator.isLoading.value ? 1 : 0,
|
||||
background: props.color || undefined,
|
||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||
transition: 'width 0.1s, height 0.4s, opacity 0.4s',
|
||||
zIndex: 6,
|
||||
},
|
||||
},
|
||||
slots,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function useLoadingIndicator(opts) {
|
||||
const progress = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const step = computed(() => 10000 / opts.duration)
|
||||
|
||||
let _timer = null
|
||||
let _throttle = null
|
||||
|
||||
function start() {
|
||||
clear()
|
||||
progress.value = 0
|
||||
if (opts.throttle) {
|
||||
_throttle = setTimeout(() => {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}, opts.throttle)
|
||||
} else {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}
|
||||
}
|
||||
function finish() {
|
||||
progress.value = 100
|
||||
_hide()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
clearInterval(_timer)
|
||||
clearTimeout(_throttle)
|
||||
_timer = null
|
||||
_throttle = null
|
||||
}
|
||||
|
||||
function _increase(num) {
|
||||
progress.value = Math.min(100, progress.value + num)
|
||||
}
|
||||
|
||||
function _hide() {
|
||||
clear()
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 400)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function _startTimer() {
|
||||
_timer = setInterval(() => {
|
||||
_increase(step.value)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return {
|
||||
progress,
|
||||
isLoading,
|
||||
start,
|
||||
finish,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,27 @@
|
||||
<template>
|
||||
<div v-if="mode !== 'isolated'" ref="button" v-tooltip.right="'Minecraft accounts'" class="button-base avatar-button"
|
||||
:class="{ expanded: mode === 'expanded' }" @click="toggleMenu">
|
||||
<Avatar :size="mode === 'expanded' ? 'xs' : 'sm'" :src="selectedAccount
|
||||
? `https://mc-heads.net/avatar/${selectedAccount.username}/128`
|
||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
" />
|
||||
<div
|
||||
v-if="mode !== 'isolated'"
|
||||
ref="button"
|
||||
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||
:class="{ expanded: mode === 'expanded' }"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<Avatar
|
||||
size="36px"
|
||||
:src="
|
||||
selectedAccount
|
||||
? `https://mc-heads.net/avatar/${selectedAccount.id}/128`
|
||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col w-full">
|
||||
<span>
|
||||
<component v-if="selectedAccount" :is="getAccountType(selectedAccount)" class="vector-icon" />
|
||||
{{ selectedAccount ? selectedAccount.username : 'Select account' }}
|
||||
</span>
|
||||
<span class="text-secondary text-xs">Minecraft account</span>
|
||||
</div>
|
||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<Card v-if="showCard || mode === 'isolated'" ref="card" class="account-card"
|
||||
@@ -79,7 +96,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { PlusIcon, TrashIcon, LogInIcon, PirateIcon as Offline, MicrosoftIcon as License, MicrosoftIcon, PirateIcon } from '@modrinth/assets'
|
||||
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon, PirateIcon as Offline, MicrosoftIcon as License, MicrosoftIcon, PirateIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||
import {
|
||||
@@ -271,11 +288,11 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.account-card {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0.5rem;
|
||||
left: 5.5rem;
|
||||
margin-top: 0.5rem;
|
||||
right: 2rem;
|
||||
z-index: 11;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
@@ -356,12 +373,17 @@ onUnmounted(() => {
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
translate 0.25s ease,
|
||||
scale 0.25s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
translate: 0 -2rem;
|
||||
scale: 0.9;
|
||||
}
|
||||
|
||||
.avatar-button {
|
||||
@@ -369,9 +391,10 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-base);
|
||||
background-color: var(--color-raised-bg);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--radius-md);
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
|
||||
&.expanded {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownIcon, FolderOpenIcon, SearchIcon } from '@modrinth/assets'
|
||||
import { Button, OverflowMenu } from '@modrinth/ui'
|
||||
import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { add_project_from_path } from '@/helpers/profile.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
@@ -26,7 +26,7 @@ const handleAddContentFromFile = async () => {
|
||||
|
||||
const handleSearchContent = async () => {
|
||||
await router.push({
|
||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
||||
query: { i: props.instance.path },
|
||||
})
|
||||
}
|
||||
@@ -34,30 +34,27 @@ const handleSearchContent = async () => {
|
||||
|
||||
<template>
|
||||
<div class="joined-buttons">
|
||||
<Button color="primary" @click="handleSearchContent"><SearchIcon /> Add content </Button>
|
||||
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'search',
|
||||
action: handleSearchContent,
|
||||
},
|
||||
{
|
||||
id: 'from_file',
|
||||
action: handleAddContentFromFile,
|
||||
},
|
||||
]"
|
||||
class="btn btn-primary btn-dropdown-animation icon-only"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #search>
|
||||
<SearchIcon />
|
||||
<span class="no-wrap"> Search </span>
|
||||
</template>
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
<ButtonStyled>
|
||||
<button @click="handleSearchContent">
|
||||
<PlusIcon />
|
||||
Install content
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'from_file',
|
||||
action: handleAddContentFromFile,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="breadcrumbs">
|
||||
<Button class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
||||
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
|
||||
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
||||
<ChevronLeftIcon />
|
||||
</Button>
|
||||
<Button class="breadcrumbs__forward transparent" icon-only @click="$router.forward()">
|
||||
<Button
|
||||
v-if="false"
|
||||
class="breadcrumbs__forward transparent"
|
||||
icon-only
|
||||
@click="$router.forward()"
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
|
||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
:to="{
|
||||
@@ -20,13 +25,18 @@
|
||||
: breadcrumb.name
|
||||
}}
|
||||
</router-link>
|
||||
<span v-else class="selected">{{
|
||||
breadcrumb.name.charAt(0) === '?'
|
||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||
: breadcrumb.name
|
||||
}}</span>
|
||||
<ChevronRightIcon v-if="breadcrumb.link" class="chevron" />
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
data-tauri-drag-region
|
||||
class="text-contrast font-semibold cursor-default select-none"
|
||||
>{{
|
||||
breadcrumb.name.charAt(0) === '?'
|
||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||
: breadcrumb.name
|
||||
}}</span
|
||||
>
|
||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,38 +60,3 @@ const breadcrumbs = computed(() => {
|
||||
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.breadcrumbs__item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
vertical-align: center;
|
||||
margin: auto 0;
|
||||
|
||||
.chevron,
|
||||
a {
|
||||
margin: auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs__back,
|
||||
.breadcrumbs__forward {
|
||||
margin: auto 0;
|
||||
color: var(--color-base);
|
||||
height: unset;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.breadcrumbs__forward {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -323,7 +323,6 @@ async function repairInstance() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
padding: var(--gap-lg);
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
|
||||
@@ -211,7 +211,6 @@ const exportPack = async () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-body {
|
||||
padding: var(--gap-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
@@ -286,6 +285,7 @@ const exportPack = async () => {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref, computed } from 'vue'
|
||||
import { onUnmounted, ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { StopCircleIcon, PlayIcon } from '@modrinth/assets'
|
||||
import { Card, Avatar, AnimatedLogo } from '@modrinth/ui'
|
||||
import { SpinnerIcon, GameIcon, TimerIcon, StopCircleIcon, PlayIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
@@ -11,6 +11,11 @@ import { handleError } from '@/store/state.js'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
@@ -19,16 +24,30 @@ const props = defineProps({
|
||||
return {}
|
||||
},
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
first: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const playing = ref(false)
|
||||
|
||||
const modLoading = computed(() => props.instance.install_stage !== 'installed')
|
||||
const loading = ref(false)
|
||||
const modLoading = computed(
|
||||
() =>
|
||||
loading.value ||
|
||||
currentEvent.value === 'installing' ||
|
||||
(currentEvent.value === 'launched' && !playing.value),
|
||||
)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const seeInstance = async () => {
|
||||
await router.push(`/instance/${encodeURIComponent(props.instance.path)}/`)
|
||||
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
||||
}
|
||||
|
||||
const checkProcess = async () => {
|
||||
@@ -39,17 +58,17 @@ const checkProcess = async () => {
|
||||
|
||||
const play = async (e, context) => {
|
||||
e?.stopPropagation()
|
||||
modLoading.value = true
|
||||
await run(props.instance.path).catch((err) =>
|
||||
handleSevereError(err, { profilePath: props.instance.path }),
|
||||
)
|
||||
modLoading.value = false
|
||||
|
||||
trackEvent('InstancePlay', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
source: context,
|
||||
})
|
||||
loading.value = true
|
||||
await run(props.instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
source: context,
|
||||
})
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const stop = async (e, context) => {
|
||||
@@ -85,175 +104,120 @@ defineExpose({
|
||||
instance: props.instance,
|
||||
})
|
||||
|
||||
const currentEvent = ref(null)
|
||||
|
||||
const unlisten = await process_listener((e) => {
|
||||
if (e.event === 'finished' && e.profile_path_id === props.instance.path) playing.value = false
|
||||
if (e.profile_path_id === props.instance.path) {
|
||||
currentEvent.value = e.event
|
||||
if (e.event === 'finished') {
|
||||
playing.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => checkProcess())
|
||||
onUnmounted(() => unlisten())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="instance">
|
||||
<Card class="instance-card-item button-base" @click="seeInstance" @mouseenter="checkProcess">
|
||||
<Avatar
|
||||
size="lg"
|
||||
:src="props.instance.icon_path ? convertFileSrc(props.instance.icon_path) : null"
|
||||
alt="Mod card"
|
||||
class="mod-image"
|
||||
/>
|
||||
<div class="project-info">
|
||||
<p class="title">{{ props.instance.name }}</p>
|
||||
<p
|
||||
v-if="
|
||||
props.instance.install_stage === 'installing' ||
|
||||
props.instance.install_stage === 'not_installed' ||
|
||||
props.instance.install_stage === 'pack_installing'
|
||||
"
|
||||
class="description"
|
||||
>
|
||||
Installing...
|
||||
</p>
|
||||
<p v-else class="description">
|
||||
{{ props.instance.loader }}
|
||||
{{ props.instance.game_version }}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<template v-if="compact">
|
||||
<div
|
||||
v-if="playing === true"
|
||||
class="stop cta button-base"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
||||
@click="seeInstance"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
<Avatar
|
||||
size="48px"
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:tint-by="instance.path"
|
||||
alt="Mod card"
|
||||
/>
|
||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
||||
<button v-tooltip="'Instance is loading...'" disabled>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<!-- Translate for optical centering -->
|
||||
<PlayIcon class="translate-x-[1px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||
<TimerIcon />
|
||||
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="modLoading === true && playing === false" class="cta loading-cta">
|
||||
<AnimatedLogo class="loading-indicator" />
|
||||
</div>
|
||||
<div v-else class="install cta button-base" @click="(e) => play(e, 'InstanceCard')">
|
||||
<PlayIcon />
|
||||
</template>
|
||||
<div v-else>
|
||||
<div
|
||||
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
||||
@click="seeInstance"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<div class="relative flex items-center justify-center">
|
||||
<Avatar
|
||||
size="48px"
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:tint-by="instance.path"
|
||||
alt="Mod card"
|
||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
||||
<button
|
||||
v-tooltip="'Stop'"
|
||||
:class="{ 'scale-100 opacity-100': playing }"
|
||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SpinnerIcon
|
||||
v-else-if="modLoading || installing"
|
||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||
class="animate-spin w-8 h-8"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<ButtonStyled v-else size="large" color="brand" circular>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<PlayIcon class="translate-x-[2px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
||||
{{ instance.name }}
|
||||
</p>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||
<GameIcon class="shrink-0" />
|
||||
<span class="text-sm">
|
||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.loading-indicator {
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
|
||||
svg {
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.instance {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.cta {
|
||||
opacity: 1;
|
||||
bottom: calc(var(--gap-md) + 4.25rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cta {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
z-index: 1;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
right: calc(var(--gap-md) * 2);
|
||||
bottom: 3.25rem;
|
||||
opacity: 0;
|
||||
transition:
|
||||
0.2s ease-in-out bottom,
|
||||
0.2s ease-in-out opacity,
|
||||
0.1s ease-in-out filter !important;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-floating);
|
||||
|
||||
svg {
|
||||
color: var(--color-accent-contrast);
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem !important;
|
||||
}
|
||||
|
||||
&.install {
|
||||
background: var(--color-brand);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.stop {
|
||||
background: var(--color-red);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.loading-cta {
|
||||
background: hsl(220, 11%, 10%) !important;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-card-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: var(--gap-md);
|
||||
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
|
||||
margin-bottom: 0;
|
||||
|
||||
.mod-image {
|
||||
--size: 100%;
|
||||
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-width: unset !important;
|
||||
max-height: unset !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
color: var(--color-contrast);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
line-height: 110%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--color-base);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 0.775rem;
|
||||
line-height: 125%;
|
||||
margin: 0.25rem 0 0;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -237,7 +237,7 @@ const display_icon = ref(null)
|
||||
const showAdvanced = ref(false)
|
||||
const creating = ref(false)
|
||||
const showSnapshots = ref(false)
|
||||
const creationType = ref('from file')
|
||||
const creationType = ref('custom')
|
||||
const isShowing = ref(false)
|
||||
|
||||
defineExpose({
|
||||
@@ -525,8 +525,8 @@ const next = async () => {
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--gap-lg);
|
||||
gap: var(--gap-md);
|
||||
margin-top: var(--gap-lg);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
@@ -595,7 +595,6 @@ const next = async () => {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--gap-lg);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
53
apps/app-frontend/src/components/ui/InstanceIndicator.vue
Normal file
53
apps/app-frontend/src/components/ui/InstanceIndicator.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
type Instance = {
|
||||
game_version: string
|
||||
loader: string
|
||||
path: string
|
||||
install_stage: string
|
||||
icon_path?: string
|
||||
name: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
instance: Instance
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
||||
<router-link
|
||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||
tabindex="-1"
|
||||
class="flex flex-col gap-4 text-primary"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||
:alt="instance.name"
|
||||
size="48px"
|
||||
/>
|
||||
<span class="flex flex-col gap-2">
|
||||
<span class="font-extrabold bold text-contrast">
|
||||
{{ instance.name }}
|
||||
</span>
|
||||
<span class="text-secondary flex items-center gap-2 font-semibold">
|
||||
<GameIcon class="h-5 w-5 text-secondary" />
|
||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</router-link>
|
||||
<ButtonStyled>
|
||||
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
|
||||
<LeftArrowIcon /> Back to instance
|
||||
</router-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalWrapper ref="detectJavaModal" header="Select java version">
|
||||
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
||||
<div class="auto-detect-modal">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
@@ -73,8 +73,6 @@ function setJavaInstall(javaInstall) {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.auto-detect-modal {
|
||||
padding: 1rem;
|
||||
|
||||
.table {
|
||||
.table-row {
|
||||
grid-template-columns: 1fr 4fr min-content;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
Auto detect
|
||||
Detect
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
@@ -187,6 +187,7 @@ async function reinstallJava() {
|
||||
|
||||
.toggle-setting {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@@ -24,6 +24,8 @@ defineExpose({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['finish-install'])
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
return props.versions
|
||||
})
|
||||
@@ -34,9 +36,17 @@ const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const inProgress = ref(false)
|
||||
|
||||
const switchVersion = async (versionId) => {
|
||||
modpackVersionModal.value.hide()
|
||||
inProgress.value = true
|
||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||
inProgress.value = false
|
||||
emit('finish-install')
|
||||
}
|
||||
|
||||
const onHide = () => {
|
||||
if (!inProgress.value) {
|
||||
emit('finish-install')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -45,9 +55,10 @@ const switchVersion = async (versionId) => {
|
||||
ref="modpackVersionModal"
|
||||
class="modpack-version-modal"
|
||||
header="Change modpack version"
|
||||
:on-hide="onHide"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<Card v-if="instance.linked_data" class="mod-card">
|
||||
<div v-if="instance.linked_data" class="mod-card">
|
||||
<div class="table">
|
||||
<div class="table-row with-columns table-head">
|
||||
<div class="table-cell table-text download-cell" />
|
||||
@@ -106,7 +117,7 @@ const switchVersion = async (versionId) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
@@ -173,7 +184,6 @@ const switchVersion = async (versionId) => {
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--gap-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
|
||||
59
apps/app-frontend/src/components/ui/NavButton.vue
Normal file
59
apps/app-frontend/src/components/ui/NavButton.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
v-if="typeof to === 'string'"
|
||||
:to="to"
|
||||
v-bind="$attrs"
|
||||
:class="{
|
||||
'router-link-active': isPrimary && isPrimary(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"
|
||||
>
|
||||
<slot />
|
||||
</RouterLink>
|
||||
<button
|
||||
v-else
|
||||
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"
|
||||
@click="to"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
||||
|
||||
defineProps<{
|
||||
to: (() => void) | string
|
||||
isPrimary?: RouteFunction
|
||||
isSubpage?: RouteFunction
|
||||
highlightOverride?: boolean
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.router-link-active,
|
||||
.subpage-active {
|
||||
svg {
|
||||
filter: drop-shadow(0 0 0.5rem black);
|
||||
}
|
||||
}
|
||||
|
||||
.router-link-active {
|
||||
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
||||
}
|
||||
|
||||
.subpage-active {
|
||||
@apply text-contrast bg-button-bg;
|
||||
}
|
||||
</style>
|
||||
160
apps/app-frontend/src/components/ui/NavTabs.vue
Normal file
160
apps/app-frontend/src/components/ui/NavTabs.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<nav
|
||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
>
|
||||
<RouterLink
|
||||
v-for="(link, index) in filteredLinks"
|
||||
v-show="link.shown === undefined ? true : link.shown"
|
||||
:key="index"
|
||||
ref="tabLinkElements"
|
||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||
:class="`button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full ${activeIndex === index && !subpageSelected ? 'text-button-textSelected' : activeIndex === index && subpageSelected ? 'text-contrast' : 'text-primary'}`"
|
||||
>
|
||||
<component :is="link.icon" v-if="link.icon" class="size-5" />
|
||||
<span class="text-nowrap">{{ link.label }}</span>
|
||||
</RouterLink>
|
||||
<div
|
||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'}`"
|
||||
:style="{
|
||||
left: sliderLeftPx,
|
||||
top: sliderTopPx,
|
||||
right: sliderRightPx,
|
||||
bottom: sliderBottomPx,
|
||||
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import { useRoute, RouterLink } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
interface Tab {
|
||||
label: string
|
||||
href: string | RouteLocationRaw
|
||||
shown?: boolean
|
||||
icon?: unknown
|
||||
subpages?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
links: Tab[]
|
||||
query?: string
|
||||
}>()
|
||||
|
||||
const sliderLeft = ref(4)
|
||||
const sliderTop = ref(4)
|
||||
const sliderRight = ref(4)
|
||||
const sliderBottom = ref(4)
|
||||
const activeIndex = ref(-1)
|
||||
const oldIndex = ref(-1)
|
||||
const subpageSelected = ref(false)
|
||||
|
||||
const filteredLinks = computed(() =>
|
||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||
)
|
||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||
|
||||
function pickLink() {
|
||||
let index = -1
|
||||
subpageSelected.value = false
|
||||
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
||||
const link = filteredLinks.value[i]
|
||||
|
||||
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
|
||||
index = i
|
||||
break
|
||||
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
|
||||
index = i
|
||||
subpageSelected.value = true
|
||||
break
|
||||
}
|
||||
}
|
||||
activeIndex.value = index
|
||||
|
||||
if (activeIndex.value !== -1) {
|
||||
startAnimation()
|
||||
} else {
|
||||
oldIndex.value = -1
|
||||
sliderLeft.value = 0
|
||||
sliderRight.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const tabLinkElements = ref()
|
||||
|
||||
function startAnimation() {
|
||||
const el = tabLinkElements.value[activeIndex.value].$el
|
||||
|
||||
if (!el || !el.offsetParent) return
|
||||
|
||||
const newValues = {
|
||||
left: el.offsetLeft,
|
||||
top: el.offsetTop,
|
||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||
}
|
||||
|
||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||
sliderLeft.value = newValues.left
|
||||
sliderRight.value = newValues.right
|
||||
sliderTop.value = newValues.top
|
||||
sliderBottom.value = newValues.bottom
|
||||
} else {
|
||||
const delay = 200
|
||||
|
||||
if (newValues.left < sliderLeft.value) {
|
||||
sliderLeft.value = newValues.left
|
||||
setTimeout(() => {
|
||||
sliderRight.value = newValues.right
|
||||
}, delay)
|
||||
} else {
|
||||
sliderRight.value = newValues.right
|
||||
setTimeout(() => {
|
||||
sliderLeft.value = newValues.left
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (newValues.top < sliderTop.value) {
|
||||
sliderTop.value = newValues.top
|
||||
setTimeout(() => {
|
||||
sliderBottom.value = newValues.bottom
|
||||
}, delay)
|
||||
} else {
|
||||
sliderBottom.value = newValues.bottom
|
||||
setTimeout(() => {
|
||||
sliderTop.value = newValues.top
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', pickLink)
|
||||
pickLink()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', pickLink)
|
||||
})
|
||||
|
||||
watch(route, () => {
|
||||
pickLink()
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.navtabs-transition {
|
||||
/* Delay on opacity is to hide any jankiness as the page loads */
|
||||
transition:
|
||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup>
|
||||
import { Card, Avatar, Button } from '@modrinth/ui'
|
||||
import { DownloadIcon, HeartIcon, CalendarIcon } from '@modrinth/assets'
|
||||
import { Avatar, TagItem } from '@modrinth/ui'
|
||||
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const router = useRouter()
|
||||
const installing = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@@ -22,6 +20,17 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const featuredCategory = computed(() => {
|
||||
if (props.project.categories.includes('optimization')) {
|
||||
return 'optimization'
|
||||
}
|
||||
|
||||
if (props.project.categories.length > 0) {
|
||||
return props.project.categories[0]
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const toColor = computed(() => {
|
||||
let color = props.project.color
|
||||
|
||||
@@ -47,197 +56,66 @@ const toTransparent = computed(() => {
|
||||
'))'
|
||||
)
|
||||
})
|
||||
|
||||
const install = async (e) => {
|
||||
e?.stopPropagation()
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
props.project.project_id,
|
||||
null,
|
||||
props.instance ? props.instance.path : null,
|
||||
'ProjectCard',
|
||||
() => {
|
||||
installing.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<Card class="project-card button-base" @click="router.push(`/project/${project.slug}`)">
|
||||
<div
|
||||
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
||||
@click="router.push(`/project/${project.slug}`)"
|
||||
>
|
||||
<div
|
||||
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
||||
'background-image': `url(${
|
||||
project.featured_gallery ??
|
||||
project.gallery[0] ??
|
||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||
})`,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="banner"
|
||||
:style="{
|
||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
||||
'background-image': `url(${
|
||||
project.featured_gallery ??
|
||||
project.gallery[0] ??
|
||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||
})`,
|
||||
class="badges-wrapper"
|
||||
:class="{
|
||||
'no-image': !project.featured_gallery && !project.gallery[0],
|
||||
}"
|
||||
>
|
||||
<div class="badges">
|
||||
<div class="badge">
|
||||
<DownloadIcon />
|
||||
{{ formatNumber(project.downloads) }}
|
||||
</div>
|
||||
<div class="badge">
|
||||
<HeartIcon />
|
||||
{{ formatNumber(project.follows) }}
|
||||
</div>
|
||||
<div class="badge">
|
||||
<CalendarIcon />
|
||||
{{ formatCategory(dayjs(project.date_modified).fromNow()) }}
|
||||
</div>
|
||||
:style="{
|
||||
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Avatar size="48px" :src="project.icon_url" />
|
||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||
<span class="line-clamp-2">{{ project.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
||||
<div
|
||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatNumber(project.downloads) }}
|
||||
</div>
|
||||
<div
|
||||
class="badges-wrapper"
|
||||
:class="{
|
||||
'no-image': !project.featured_gallery && !project.gallery[0],
|
||||
}"
|
||||
:style="{
|
||||
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<Avatar class="icon" size="sm" :src="project.icon_url" />
|
||||
<div class="title">
|
||||
<div class="title-text">
|
||||
{{ project.title }}
|
||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||
>
|
||||
<HeartIcon />
|
||||
{{ formatNumber(project.follows) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 pr-2">
|
||||
<TagIcon />
|
||||
<TagItem>
|
||||
{{ formatCategory(featuredCategory) }}
|
||||
</TagItem>
|
||||
</div>
|
||||
<div class="author">by {{ project.author }}</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ project.description }}
|
||||
</div>
|
||||
</Card>
|
||||
<Button color="primary" class="install" :disabled="installing" @click="install">
|
||||
<DownloadIcon />
|
||||
{{ installing ? 'Installing' : 'Install' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
|
||||
&:hover {
|
||||
.install:enabled {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-card {
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-template:
|
||||
'. . . .' 0
|
||||
'. icon title .' 3rem
|
||||
'banner banner banner banner' auto
|
||||
'. description description .' 3.5rem
|
||||
'. . . .' 0 / 0 3rem minmax(0, 1fr) 0;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.icon {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
grid-area: title;
|
||||
white-space: nowrap;
|
||||
|
||||
.title-text {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
font-size: var(--font-size-sm);
|
||||
grid-area: author;
|
||||
}
|
||||
|
||||
.banner {
|
||||
grid-area: banner;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
|
||||
.badges-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mix-blend-mode: hard-light;
|
||||
}
|
||||
|
||||
.badges {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--gap-sm);
|
||||
gap: var(--gap-xs);
|
||||
display: flex;
|
||||
z-index: 10;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
grid-area: description;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: var(--color-raised-bg);
|
||||
font-size: var(--font-size-xs);
|
||||
padding: var(--gap-xs) var(--gap-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: var(--gap-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.install {
|
||||
position: absolute;
|
||||
top: calc(5rem + var(--gap-sm));
|
||||
right: var(--gap-sm);
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { list } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import dayjs from 'dayjs'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { profile_listener } from '@/helpers/events.js'
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
|
||||
const recentInstances = ref([])
|
||||
const getInstances = async () => {
|
||||
const profiles = await list().catch(handleError)
|
||||
|
||||
recentInstances.value = profiles
|
||||
.sort((a, b) => {
|
||||
const dateACreated = dayjs(a.created)
|
||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||
|
||||
const dateBCreated = dayjs(b.created)
|
||||
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
||||
|
||||
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
||||
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
||||
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
.slice(0, 4)
|
||||
}
|
||||
|
||||
await getInstances()
|
||||
|
||||
const unlistenProfile = await profile_listener(async (event) => {
|
||||
if (event.event !== 'synced') {
|
||||
await getInstances()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavButton
|
||||
v-for="instance in recentInstances"
|
||||
:key="instance.id"
|
||||
v-tooltip.right="instance.name"
|
||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||
class="relative"
|
||||
>
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
size="28px"
|
||||
:tint-by="instance.path"
|
||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div
|
||||
v-if="instance.install_stage !== 'installed'"
|
||||
class="absolute inset-0 flex items-center justify-center z-10"
|
||||
>
|
||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||
</div>
|
||||
</NavButton>
|
||||
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,15 +1,12 @@
|
||||
<template>
|
||||
<div class="action-groups">
|
||||
<a href="https://support.modrinth.com" class="link">
|
||||
<ChatIcon />
|
||||
<span> Get support </span>
|
||||
</a>
|
||||
<Button v-if="currentLoadingBars.length > 0" ref="infoButton" icon-only class="icon-button show-card-icon"
|
||||
@click="toggleCard()">
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
|
||||
<button ref="infoButton" @click="toggleCard()">
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-if="offline" class="status">
|
||||
<span class="circle stopped" />
|
||||
<UnplugIcon />
|
||||
<div class="running-text">
|
||||
<span> Offline </span>
|
||||
</div>
|
||||
@@ -31,10 +28,6 @@
|
||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
||||
<TerminalSquareIcon />
|
||||
</Button>
|
||||
<Button v-if="currentLoadingBars.length > 0" ref="infoButton" icon-only class="icon-button show-card-icon"
|
||||
@click="toggleCard()">
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="status">
|
||||
<span class="circle stopped" />
|
||||
@@ -105,8 +98,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, StopCircleIcon, TerminalSquareIcon, DropdownIcon } from '@modrinth/assets'
|
||||
import { Button, Card } from '@modrinth/ui'
|
||||
import {
|
||||
DownloadIcon,
|
||||
StopCircleIcon,
|
||||
TerminalSquareIcon,
|
||||
DropdownIcon,
|
||||
UnplugIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, ButtonStyled, Card } from '@modrinth/ui'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||
import { loading_listener, process_listener } from '@/helpers/events'
|
||||
@@ -114,7 +113,6 @@ import { useRouter } from 'vue-router'
|
||||
import { progress_bars_list } from '@/helpers/state.js'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { ChatIcon } from '@/assets/icons'
|
||||
import { get_many } from '@/helpers/profile.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { version } from '../../../package.json'
|
||||
@@ -520,10 +518,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.show-card-icon {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.download-enter-active,
|
||||
.download-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
@@ -1,74 +1,128 @@
|
||||
<template>
|
||||
<Card
|
||||
class="card button-base"
|
||||
<div
|
||||
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
||||
@click="
|
||||
() => {
|
||||
emits('open')
|
||||
emit('open')
|
||||
$router.push({
|
||||
path: `/project/${project.project_id ?? project.id}/`,
|
||||
path: `/project/${project.project_id ?? project.id}`,
|
||||
query: { i: props.instance ? props.instance.path : undefined },
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="icon">
|
||||
<Avatar :src="project.icon_url" size="md" class="search-icon" />
|
||||
<div class="icon w-[96px] h-[96px] relative">
|
||||
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="title joined-text">
|
||||
<h2>{{ project.title }}</h2>
|
||||
<span v-if="project.author">by {{ project.author }}</span>
|
||||
<div class="flex flex-col gap-2 overflow-hidden">
|
||||
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
||||
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
|
||||
{{ project.title }}
|
||||
</span>
|
||||
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<div class="m-0 line-clamp-2">
|
||||
{{ project.description }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
<Categories :categories="categories" :type="project.project_type">
|
||||
<EnvironmentIndicator
|
||||
:type-only="project.moderation"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:type="project.project_type"
|
||||
:search="true"
|
||||
/>
|
||||
</Categories>
|
||||
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
||||
<TagsIcon class="h-4 w-4 shrink-0" />
|
||||
<div
|
||||
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||
>
|
||||
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
||||
Client or server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(project.client_side === 'optional' || project.client_side === 'required') &&
|
||||
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
||||
"
|
||||
>
|
||||
Client
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(project.server_side === 'optional' || project.server_side === 'required') &&
|
||||
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
||||
"
|
||||
>
|
||||
Server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
||||
"
|
||||
>
|
||||
Unsupported
|
||||
</template>
|
||||
<template
|
||||
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
||||
>
|
||||
Client and server
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-for="tag in categories"
|
||||
:key="tag"
|
||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||
>
|
||||
{{ formatCategory(tag.name) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats button-group">
|
||||
<div v-if="featured" class="badge">
|
||||
<StarIcon />
|
||||
Featured
|
||||
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
||||
<div class="flex items-center gap-2">
|
||||
<DownloadIcon class="shrink-0" />
|
||||
<span>
|
||||
{{ formatNumber(project.downloads) }}
|
||||
<span class="text-secondary">downloads</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="badge">
|
||||
<DownloadIcon />
|
||||
{{ formatNumber(project.downloads) }}
|
||||
<div class="flex items-center gap-2">
|
||||
<HeartIcon class="shrink-0" />
|
||||
<span>
|
||||
{{ formatNumber(project.follows ?? project.followers) }}
|
||||
<span class="text-secondary">followers</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="badge">
|
||||
<HeartIcon />
|
||||
{{ formatNumber(project.follows ?? project.followers) }}
|
||||
</div>
|
||||
<div class="badge">
|
||||
<CalendarIcon />
|
||||
{{ formatCategory(dayjs(project.date_modified ?? project.updated).fromNow()) }}
|
||||
<div class="mt-auto relative">
|
||||
<div class="absolute bottom-0 right-0 w-fit">
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<button
|
||||
:disabled="installed || installing"
|
||||
class="shrink-0 no-wrap"
|
||||
@click.stop="install()"
|
||||
>
|
||||
<template v-if="!installed">
|
||||
<DownloadIcon v-if="modpack || instance" />
|
||||
<PlusIcon v-else />
|
||||
</template>
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing'
|
||||
: installed
|
||||
? 'Installed'
|
||||
: modpack || instance
|
||||
? 'Install'
|
||||
: 'Add to an instance'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="project.author" class="install">
|
||||
<Button color="primary" :disabled="installed || installing" @click.stop="install()">
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installing ? 'Installing' : installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, HeartIcon, CalendarIcon, CheckIcon, StarIcon } from '@modrinth/assets'
|
||||
import { Avatar, Card, Categories, EnvironmentIndicator, Button } from '@modrinth/ui'
|
||||
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
@@ -99,99 +153,23 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['open'])
|
||||
const emit = defineEmits(['open', 'install'])
|
||||
|
||||
const installing = ref(false)
|
||||
const installed = ref(props.installed)
|
||||
|
||||
async function install() {
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
props.project.project_id,
|
||||
props.project.project_id ?? props.project.id,
|
||||
null,
|
||||
props.instance ? props.instance.path : null,
|
||||
'SearchCard',
|
||||
(version) => {
|
||||
() => {
|
||||
installing.value = false
|
||||
|
||||
if (props.instance && version) {
|
||||
installed.value = true
|
||||
}
|
||||
emit('install', props.project.project_id ?? props.project.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const modpack = computed(() => props.project.project_type === 'modpack')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-self: center;
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
grid-column: 2 / 4;
|
||||
flex-direction: column;
|
||||
grid-row: 1;
|
||||
gap: 0.5rem;
|
||||
|
||||
.description {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
justify-self: stretch;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.install {
|
||||
grid-column: 3 / 4;
|
||||
grid-row: 2;
|
||||
justify-self: end;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 6rem auto 7rem;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
|
||||
&:active:not(&:disabled) {
|
||||
scale: 0.98 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.joined-text {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
column-gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0 !important;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<ProgressBar class="loading-bar" :progress="loadingProgress" />
|
||||
<ProgressBar class="loading-bar" :progress="Math.min(loadingProgress, 100)" />
|
||||
<span v-if="message">{{ message }}</span>
|
||||
</div>
|
||||
<div class="gradient-bg" data-tauri-drag-region></div>
|
||||
@@ -86,8 +86,7 @@ import { ref, watch } from 'vue'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { loading_listener } from '@/helpers/events.js'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { MaximizeIcon, MinimizeIcon } from '@/assets/icons/index.js'
|
||||
import { XIcon, MaximizeIcon, MinimizeIcon } from '@modrinth/assets'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
import { useLoading } from '@/store/loading.js'
|
||||
|
||||
@@ -128,7 +127,6 @@ const os = ref('')
|
||||
getOS().then((x) => (os.value = x))
|
||||
|
||||
loading_listener(async (e) => {
|
||||
console.log(e)
|
||||
if (e.event.type === 'directory_move') {
|
||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||
message.value = 'Updating app directory...'
|
||||
|
||||
@@ -71,7 +71,6 @@ async function install() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--gap-md);
|
||||
padding: var(--gap-lg);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
|
||||
357
apps/app-frontend/src/components/ui/friends/FriendsList.vue
Normal file
357
apps/app-frontend/src/components/ui/friends/FriendsList.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<script setup lang="ts">
|
||||
import { Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import {
|
||||
UserPlusIcon,
|
||||
MoreVerticalIcon,
|
||||
MailIcon,
|
||||
SettingsIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ref, onUnmounted, watch, computed } from 'vue'
|
||||
import { friend_listener } from '@/helpers/events'
|
||||
import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
|
||||
import { get_user_many } from '@/helpers/cache'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
credentials: unknown | null
|
||||
signIn: () => void2
|
||||
}>()
|
||||
|
||||
const userCredentials = computed(() => props.credentials)
|
||||
|
||||
const search = ref('')
|
||||
const manageFriendsModal = ref()
|
||||
const friendInvitesModal = ref()
|
||||
|
||||
const username = ref('')
|
||||
const addFriendModal = ref()
|
||||
async function addFriendFromModal() {
|
||||
addFriendModal.value.hide()
|
||||
await add_friend(username.value).catch(handleError)
|
||||
username.value = ''
|
||||
await loadFriends()
|
||||
}
|
||||
|
||||
const friendOptions = ref()
|
||||
async function handleFriendOptions(args) {
|
||||
switch (args.option) {
|
||||
case 'remove-friend':
|
||||
await removeFriend(args.item)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function addFriend(friend: Friend) {
|
||||
await add_friend(
|
||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
||||
).catch(handleError)
|
||||
await loadFriends()
|
||||
}
|
||||
|
||||
async function removeFriend(friend: Friend) {
|
||||
await remove_friend(
|
||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
||||
).catch(handleError)
|
||||
await loadFriends()
|
||||
}
|
||||
|
||||
type Friend = {
|
||||
id: string
|
||||
friend_id: string | null
|
||||
status: string | null
|
||||
last_updated: Dayjs | null
|
||||
created: Dayjs
|
||||
username: string
|
||||
accepted: boolean
|
||||
online: boolean
|
||||
avatar: string
|
||||
}
|
||||
|
||||
const userFriends = ref<Friend[]>([])
|
||||
const acceptedFriends = computed(() =>
|
||||
userFriends.value
|
||||
.filter((x) => x.accepted)
|
||||
.toSorted((a, b) => {
|
||||
if (a.last_updated === null && b.last_updated === null) {
|
||||
return 0 // Both are null, equal in sorting
|
||||
}
|
||||
if (a.last_updated === null) {
|
||||
return 1 // `a` is null, move it after `b`
|
||||
}
|
||||
if (b.last_updated === null) {
|
||||
return -1 // `b` is null, move it after `a`
|
||||
}
|
||||
// Both are non-null, sort by date
|
||||
return b.last_updated.diff(a.last_updated)
|
||||
}),
|
||||
)
|
||||
const pendingFriends = computed(() =>
|
||||
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
|
||||
)
|
||||
|
||||
const loading = ref(true)
|
||||
async function loadFriends(timeout = false) {
|
||||
loading.value = timeout
|
||||
|
||||
try {
|
||||
const friendsList = await friends()
|
||||
|
||||
if (friendsList.length === 0) {
|
||||
userFriends.value = []
|
||||
} else {
|
||||
const friendStatuses = await friend_statuses()
|
||||
const users = await get_user_many(
|
||||
friendsList.map((x) => (x.id === userCredentials.value.user_id ? x.friend_id : x.id)),
|
||||
)
|
||||
|
||||
userFriends.value = friendsList.map((friend) => {
|
||||
const user = users.find((x) => x.id === friend.id || x.id === friend.friend_id)
|
||||
const status = friendStatuses.find(
|
||||
(x) => x.user_id === friend.id || x.user_id === friend.friend_id,
|
||||
)
|
||||
return {
|
||||
id: friend.id,
|
||||
friend_id: friend.friend_id,
|
||||
status: status?.profile_name,
|
||||
last_updated: status && status.last_update ? dayjs(status.last_update) : null,
|
||||
created: dayjs(friend.created),
|
||||
avatar: user?.avatar_url,
|
||||
username: user?.username,
|
||||
online: !!status,
|
||||
accepted: friend.accepted,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
} catch (e) {
|
||||
console.error('Error loading friends', e)
|
||||
if (timeout) {
|
||||
setTimeout(() => loadFriends(), 15 * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
userCredentials,
|
||||
() => {
|
||||
if (userCredentials.value === undefined) {
|
||||
userFriends.value = []
|
||||
} else if (userCredentials.value === null) {
|
||||
userFriends.value = []
|
||||
loading.value = false
|
||||
} else {
|
||||
loadFriends(true)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const unlisten = await friend_listener(() => loadFriends())
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
|
||||
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
|
||||
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
||||
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
||||
<div
|
||||
v-for="friend in acceptedFriends.filter(
|
||||
(x) => !search || x.username.toLowerCase().includes(search),
|
||||
)"
|
||||
:key="friend.username"
|
||||
class="flex gap-2 items-center"
|
||||
>
|
||||
<div class="relative">
|
||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||
<span
|
||||
v-if="friend.online"
|
||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ friend.username }}</div>
|
||||
<div class="ml-auto">
|
||||
<ButtonStyled>
|
||||
<button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Remove
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
||||
<p v-if="pendingFriends.length === 0">You have no pending friend requests :C</p>
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<div v-for="friend in pendingFriends" :key="friend.username" class="flex gap-2">
|
||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<p class="m-0">
|
||||
<template v-if="friend.id === userCredentials.user_id">
|
||||
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
|
||||
</template>
|
||||
<template v-else>
|
||||
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||
</template>
|
||||
</p>
|
||||
<p class="m-0 text-sm text-secondary">{{ friend.created.fromNow() }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<template v-if="friend.id === userCredentials.user_id">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="addFriend(friend)">
|
||||
<UserPlusIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Ignore
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled>
|
||||
<button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
||||
<div class="mb-4">
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
|
||||
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
||||
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
|
||||
</div>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||
<UserPlusIcon />
|
||||
Add friend
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</ModalWrapper>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg m-0">Friends</h3>
|
||||
<ButtonStyled v-if="userCredentials" type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'add-friend',
|
||||
action: () => addFriendModal.show(),
|
||||
},
|
||||
{
|
||||
id: 'manage-friends',
|
||||
action: () => manageFriendsModal.show(),
|
||||
shown: acceptedFriends.length > 0,
|
||||
},
|
||||
{
|
||||
id: 'view-requests',
|
||||
action: () => friendInvitesModal.show(),
|
||||
shown: pendingFriends.length > 0,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #add-friend>
|
||||
<UserPlusIcon aria-hidden="true" />
|
||||
Add friend
|
||||
</template>
|
||||
<template #manage-friends>
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
Manage friends
|
||||
<div
|
||||
v-if="acceptedFriends.length > 0"
|
||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
||||
>
|
||||
{{ acceptedFriends.length }}
|
||||
</div>
|
||||
</template>
|
||||
<template #view-requests>
|
||||
<MailIcon aria-hidden="true" />
|
||||
View friend requests
|
||||
<div
|
||||
v-if="pendingFriends.length > 0"
|
||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
||||
>
|
||||
{{ pendingFriends.length }}
|
||||
</div>
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<template v-if="loading">
|
||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
||||
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
||||
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="acceptedFriends.length === 0">
|
||||
<div class="text-sm">
|
||||
<div v-if="!userCredentials">
|
||||
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
|
||||
to share what you're playing!
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
||||
<template #remove-friend> <TrashIcon /> Remove friend </template>
|
||||
</ContextMenu>
|
||||
<div
|
||||
v-for="friend in acceptedFriends.slice(0, 5)"
|
||||
:key="friend.username"
|
||||
class="flex gap-2 items-center"
|
||||
:class="{ grayscale: !friend.online }"
|
||||
@contextmenu.prevent.stop="
|
||||
(event) =>
|
||||
friendOptions.showMenu(event, friend, [
|
||||
{
|
||||
name: 'remove-friend',
|
||||
color: 'danger',
|
||||
},
|
||||
])
|
||||
"
|
||||
>
|
||||
<div class="relative">
|
||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||
<span
|
||||
v-if="friend.online"
|
||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
|
||||
{{ friend.username }}
|
||||
</span>
|
||||
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,37 +7,43 @@
|
||||
installed.
|
||||
</p>
|
||||
<table>
|
||||
<tr class="header">
|
||||
<th>{{ instance?.name }}</th>
|
||||
<th>{{ project.title }}</th>
|
||||
</tr>
|
||||
<tr class="content">
|
||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
||||
<td>
|
||||
<DropdownSelect
|
||||
v-if="versions?.length > 1"
|
||||
v-model="selectedVersion"
|
||||
:options="versions"
|
||||
placeholder="Select version"
|
||||
name="Version select"
|
||||
:display-name="
|
||||
(version) =>
|
||||
`${version?.name} (${version?.loaders
|
||||
.map((name) => formatCategory(name))
|
||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
||||
"
|
||||
render-up
|
||||
/>
|
||||
<span v-else>
|
||||
<span>
|
||||
{{ selectedVersion?.name }} ({{
|
||||
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
||||
}}
|
||||
- {{ selectedVersion?.game_versions.join(', ') }})
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th>{{ instance?.name }}</th>
|
||||
<th>{{ project.title }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="content">
|
||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
||||
<td>
|
||||
<multiselect
|
||||
v-if="versions?.length > 1"
|
||||
v-model="selectedVersion"
|
||||
:options="versions"
|
||||
:searchable="true"
|
||||
placeholder="Select version"
|
||||
open-direction="top"
|
||||
:show-labels="false"
|
||||
:custom-label="
|
||||
(version) =>
|
||||
`${version?.name} (${version?.loaders
|
||||
.map((name) => formatCategory(name))
|
||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
||||
"
|
||||
:max-height="150"
|
||||
/>
|
||||
<span v-else>
|
||||
<span>
|
||||
{{ selectedVersion?.name }} ({{
|
||||
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
||||
}}
|
||||
- {{ selectedVersion?.game_versions.join(', ') }})
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="button-group">
|
||||
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
|
||||
@@ -52,12 +58,13 @@
|
||||
<script setup>
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { Button, DropdownSelect } from '@modrinth/ui'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||
import { ref } from 'vue'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
const instance = ref(null)
|
||||
const project = ref(null)
|
||||
@@ -151,7 +158,6 @@ td:first-child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
:deep(.animated-dropdown .options) {
|
||||
max-height: 13.375rem;
|
||||
|
||||
@@ -68,6 +68,5 @@ async function install() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -243,7 +243,7 @@ const createInstance = async () => {
|
||||
"
|
||||
>
|
||||
<Button
|
||||
:disabled="profile.installedMod || profile.installing || profile.linked_data?.locked"
|
||||
:disabled="profile.installedMod || profile.installing"
|
||||
@click="install(profile)"
|
||||
>
|
||||
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
||||
@@ -253,9 +253,7 @@ const createInstance = async () => {
|
||||
? 'Installing...'
|
||||
: profile.installedMod
|
||||
? 'Installed'
|
||||
: profile.linked_data && profile.linked_data.locked
|
||||
? 'Paired'
|
||||
: 'Install'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -308,7 +306,6 @@ const createInstance = async () => {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
@@ -360,7 +357,7 @@ const createInstance = async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.profiles {
|
||||
@@ -381,7 +378,6 @@ const createInstance = async () => {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
img {
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
<script setup lang="ts">
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
|
||||
import { computed, ref, type Ref, watch } from 'vue'
|
||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const deleteConfirmModal = ref()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const title = ref(props.instance.name)
|
||||
const icon: Ref<string | undefined> = ref(props.instance.icon_path)
|
||||
const groups = ref(props.instance.groups)
|
||||
|
||||
const newCategoryInput = ref('')
|
||||
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
async function duplicateProfile() {
|
||||
await duplicate(props.instance.path).catch(handleError)
|
||||
trackEvent('InstanceDuplicate', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
const allInstances = ref((await list()) as GameInstance[])
|
||||
const availableGroups = computed(() => [
|
||||
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
||||
])
|
||||
|
||||
async function resetIcon() {
|
||||
icon.value = undefined
|
||||
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
|
||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
||||
|
||||
trackEvent('InstanceSetIcon')
|
||||
}
|
||||
|
||||
const editProfileObject = computed(() => ({
|
||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||
}))
|
||||
|
||||
const toggleGroup = (group: string) => {
|
||||
if (groups.value.includes(group)) {
|
||||
groups.value = groups.value.filter((x) => x !== group)
|
||||
} else {
|
||||
groups.value.push(group)
|
||||
}
|
||||
}
|
||||
|
||||
const addCategory = () => {
|
||||
const text = newCategoryInput.value.trim()
|
||||
|
||||
if (text.length > 0) {
|
||||
groups.value.push(text.substring(0, 32))
|
||||
newCategoryInput.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[title, groups, groups],
|
||||
async () => {
|
||||
await edit(props.instance.path, editProfileObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
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 messages = defineMessages({
|
||||
name: {
|
||||
id: 'instance.settings.tabs.general.name',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
libraryGroups: {
|
||||
id: 'instance.settings.tabs.general.library-groups',
|
||||
defaultMessage: 'Library groups',
|
||||
},
|
||||
libraryGroupsDescription: {
|
||||
id: 'instance.settings.tabs.general.library-groups.description',
|
||||
defaultMessage:
|
||||
'Library groups allow you to organize your instances into different sections in your library.',
|
||||
},
|
||||
libraryGroupsEnterName: {
|
||||
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
||||
defaultMessage: 'Enter group name',
|
||||
},
|
||||
libraryGroupsCreate: {
|
||||
id: 'instance.settings.tabs.general.library-groups.create',
|
||||
defaultMessage: 'Create new group',
|
||||
},
|
||||
editIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon',
|
||||
defaultMessage: 'Edit icon',
|
||||
},
|
||||
selectIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.select',
|
||||
defaultMessage: 'Select icon',
|
||||
},
|
||||
replaceIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.replace',
|
||||
defaultMessage: 'Replace icon',
|
||||
},
|
||||
removeIcon: {
|
||||
id: 'instance.settings.tabs.general.edit-icon.remove',
|
||||
defaultMessage: 'Remove icon',
|
||||
},
|
||||
duplicateInstance: {
|
||||
id: 'instance.settings.tabs.general.duplicate-instance',
|
||||
defaultMessage: 'Duplicate instance',
|
||||
},
|
||||
duplicateInstanceDescription: {
|
||||
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
||||
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
||||
},
|
||||
duplicateButtonTooltipInstalling: {
|
||||
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
||||
defaultMessage: 'Cannot duplicate while installing.',
|
||||
},
|
||||
duplicateButton: {
|
||||
id: 'instance.settings.tabs.general.duplicate-button',
|
||||
defaultMessage: 'Duplicate',
|
||||
},
|
||||
deleteInstance: {
|
||||
id: 'instance.settings.tabs.general.delete',
|
||||
defaultMessage: 'Delete instance',
|
||||
},
|
||||
deleteInstanceDescription: {
|
||||
id: 'instance.settings.tabs.general.delete.description',
|
||||
defaultMessage:
|
||||
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
||||
},
|
||||
deleteInstanceButton: {
|
||||
id: 'instance.settings.tabs.general.delete.button',
|
||||
defaultMessage: 'Delete instance',
|
||||
},
|
||||
deletingInstanceButton: {
|
||||
id: 'instance.settings.tabs.general.deleting.button',
|
||||
defaultMessage: 'Deleting...',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmModalWrapper
|
||||
ref="deleteConfirmModal"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
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"
|
||||
proceed-label="Delete"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="removeProfile"
|
||||
/>
|
||||
<div class="block">
|
||||
<div class="float-end ml-4 relative group">
|
||||
<OverflowMenu
|
||||
v-tooltip="formatMessage(messages.editIcon)"
|
||||
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||
:options="[
|
||||
{
|
||||
id: 'select',
|
||||
action: () => setIcon(),
|
||||
},
|
||||
{
|
||||
id: 'remove',
|
||||
color: 'danger',
|
||||
action: () => resetIcon(),
|
||||
shown: !!icon,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Avatar
|
||||
:src="icon ? convertFileSrc(icon) : icon"
|
||||
size="108px"
|
||||
class="!border-4 group-hover:brightness-75"
|
||||
:tint-by="props.instance.path"
|
||||
no-shadow
|
||||
/>
|
||||
<div class="absolute top-0 right-0 m-2">
|
||||
<div
|
||||
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
||||
>
|
||||
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<template #select>
|
||||
<UploadIcon />
|
||||
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||
</template>
|
||||
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||
</OverflowMenu>
|
||||
</div>
|
||||
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.name) }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<input
|
||||
id="instance-name"
|
||||
v-model="title"
|
||||
autocomplete="off"
|
||||
maxlength="80"
|
||||
class="flex-grow"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="instance.install_stage == 'installed'">
|
||||
<div>
|
||||
<h2
|
||||
id="duplicate-instance-label"
|
||||
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
||||
>
|
||||
{{ formatMessage(messages.duplicateInstance) }}
|
||||
</h2>
|
||||
<p class="m-0 mb-2">
|
||||
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||
aria-labelledby="duplicate-instance-label"
|
||||
:disabled="installing"
|
||||
@click="duplicateProfile"
|
||||
>
|
||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.libraryGroups) }}
|
||||
</h2>
|
||||
<p class="m-0 mb-2">
|
||||
{{ formatMessage(messages.libraryGroupsDescription) }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Checkbox
|
||||
v-for="group in availableGroups"
|
||||
:key="group"
|
||||
:model-value="groups.includes(group)"
|
||||
:label="group"
|
||||
@click="toggleGroup(group)"
|
||||
/>
|
||||
<div class="flex gap-2 items-center">
|
||||
<input
|
||||
v-model="newCategoryInput"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||
@submit="() => addCategory"
|
||||
/>
|
||||
<ButtonStyled>
|
||||
<button class="w-fit" @click="() => addCategory()">
|
||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.deleteInstance) }}
|
||||
</h2>
|
||||
<p class="m-0 mb-2">
|
||||
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||
</p>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
aria-labelledby="delete-instance-label"
|
||||
:disabled="removing"
|
||||
@click="deleteConfirmModal.show()"
|
||||
>
|
||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||
<TrashIcon v-else />
|
||||
{{
|
||||
removing
|
||||
? formatMessage(messages.deletingInstanceButton)
|
||||
: formatMessage(messages.deleteInstanceButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.hovering-icon-shadow {
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { Checkbox } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||
|
||||
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 editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
hooks?: Hooks
|
||||
} = {}
|
||||
|
||||
if (overrideHooks.value) {
|
||||
editProfile.hooks = hooks.value
|
||||
}
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
watch(
|
||||
[overrideHooks, hooks],
|
||||
async () => {
|
||||
await edit(props.instance.path, editProfileObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
const messages = defineMessages({
|
||||
hooks: {
|
||||
id: 'instance.settings.tabs.hooks.title',
|
||||
defaultMessage: 'Game launch hooks',
|
||||
},
|
||||
hooksDescription: {
|
||||
id: 'instance.settings.tabs.hooks.description',
|
||||
defaultMessage:
|
||||
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
||||
},
|
||||
customHooks: {
|
||||
id: 'instance.settings.tabs.hooks.custom-hooks',
|
||||
defaultMessage: 'Custom launch hooks',
|
||||
},
|
||||
preLaunch: {
|
||||
id: 'instance.settings.tabs.hooks.pre-launch',
|
||||
defaultMessage: 'Pre-launch',
|
||||
},
|
||||
preLaunchDescription: {
|
||||
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
||||
defaultMessage: 'Ran before the instance is launched.',
|
||||
},
|
||||
preLaunchEnter: {
|
||||
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
||||
defaultMessage: 'Enter pre-launch command...',
|
||||
},
|
||||
wrapper: {
|
||||
id: 'instance.settings.tabs.hooks.wrapper',
|
||||
defaultMessage: 'Wrapper',
|
||||
},
|
||||
wrapperDescription: {
|
||||
id: 'instance.settings.tabs.hooks.wrapper.description',
|
||||
defaultMessage: 'Wrapper command for launching Minecraft.',
|
||||
},
|
||||
wrapperEnter: {
|
||||
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
||||
defaultMessage: 'Enter wrapper command...',
|
||||
},
|
||||
postExit: {
|
||||
id: 'instance.settings.tabs.hooks.post-exit',
|
||||
defaultMessage: 'Post-exit',
|
||||
},
|
||||
postExitDescription: {
|
||||
id: 'instance.settings.tabs.hooks.post-exit.description',
|
||||
defaultMessage: 'Ran after the game closes.',
|
||||
},
|
||||
postExitEnter: {
|
||||
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
||||
defaultMessage: 'Enter post-exit command...',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.hooks) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.hooksDescription) }}
|
||||
</p>
|
||||
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
||||
|
||||
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.preLaunch) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.preLaunchDescription) }}
|
||||
</p>
|
||||
<input
|
||||
id="pre-launch"
|
||||
v-model="hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||
class="w-full mt-2"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.wrapper) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.wrapperDescription) }}
|
||||
</p>
|
||||
<input
|
||||
id="wrapper"
|
||||
v-model="hooks.wrapper"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||
class="w-full mt-2"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.postExit) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.postExitDescription) }}
|
||||
</p>
|
||||
<input
|
||||
id="post-exit"
|
||||
v-model="hooks.post_exit"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.postExitEnter)"
|
||||
class="w-full mt-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,789 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
TransferIcon,
|
||||
IssuesIcon,
|
||||
HammerIcon,
|
||||
DownloadIcon,
|
||||
WrenchIcon,
|
||||
UndoIcon,
|
||||
SpinnerIcon,
|
||||
UnplugIcon,
|
||||
UnlinkIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Checkbox, Chips, ButtonStyled, TeleportDropdownMenu } from '@modrinth/ui'
|
||||
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
||||
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get_loader_versions } from '@/helpers/metadata'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import {
|
||||
formatCategory,
|
||||
type GameVersionTag,
|
||||
type PlatformTag,
|
||||
type Project,
|
||||
type Version,
|
||||
} from '@modrinth/utils'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { get_project, get_version_many } from '@/helpers/cache'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import type {
|
||||
InstanceSettingsTabProps,
|
||||
ManifestLoaderVersion,
|
||||
Manifest,
|
||||
} from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const repairConfirmModal = ref()
|
||||
const modpackVersionModal = ref()
|
||||
const modalConfirmUnpair = ref()
|
||||
const modalConfirmReinstall = ref()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const loader = ref(props.instance.loader)
|
||||
const gameVersion = ref(props.instance.game_version)
|
||||
|
||||
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((manifest: Manifest) => shallowRef(manifest))
|
||||
.catch(handleError),
|
||||
get_loader_versions('forge')
|
||||
.then((manifest: Manifest) => shallowRef(manifest))
|
||||
.catch(handleError),
|
||||
get_loader_versions('quilt')
|
||||
.then((manifest: Manifest) => shallowRef(manifest))
|
||||
.catch(handleError),
|
||||
get_loader_versions('neo')
|
||||
.then((manifest: Manifest) => shallowRef(manifest))
|
||||
.catch(handleError),
|
||||
get_game_versions()
|
||||
.then((gameVersions: GameVersionTag[]) => shallowRef(gameVersions))
|
||||
.catch(handleError),
|
||||
get_loaders()
|
||||
.then((value: PlatformTag[]) =>
|
||||
value
|
||||
.filter(
|
||||
(item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla',
|
||||
)
|
||||
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
|
||||
)
|
||||
.then((loader: PlatformTag[]) => ref(loader))
|
||||
.catch(handleError),
|
||||
])
|
||||
|
||||
const modpackProject: Ref<Project | null> = ref(null)
|
||||
const modpackVersion: Ref<Version | null> = ref(null)
|
||||
const modpackVersions: Ref<Version[] | null> = ref(null)
|
||||
const fetching = ref(true)
|
||||
|
||||
if (props.instance.linked_data && props.instance.linked_data.project_id && !props.offline) {
|
||||
get_project(props.instance.linked_data.project_id, 'must_revalidate')
|
||||
.then((project) => {
|
||||
modpackProject.value = project
|
||||
|
||||
if (project && project.versions) {
|
||||
get_version_many(project.versions, 'must_revalidate')
|
||||
.then((versions: Version[]) => {
|
||||
modpackVersions.value = versions.sort((a, b) =>
|
||||
dayjs(b.date_published).diff(dayjs(a.date_published)),
|
||||
)
|
||||
modpackVersion.value =
|
||||
versions.find(
|
||||
(version: Version) => version.id === props.instance.linked_data?.version_id,
|
||||
) ?? null
|
||||
})
|
||||
.catch(handleError)
|
||||
.finally(() => {
|
||||
fetching.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
fetching.value = false
|
||||
})
|
||||
} else {
|
||||
fetching.value = false
|
||||
}
|
||||
|
||||
const currentLoaderIcon = computed(
|
||||
() => loaders?.value.find((x) => x.name === props.instance.loader)?.icon,
|
||||
)
|
||||
|
||||
const gameVersionsForLoader = computed(() => {
|
||||
return all_game_versions?.value.filter((item) => {
|
||||
if (loader.value === 'fabric') {
|
||||
return !!fabric_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||
} else if (loader.value === 'forge') {
|
||||
return !!forge_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||
} else if (loader.value === 'quilt') {
|
||||
return !!quilt_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||
} else if (loader.value === 'neoforge') {
|
||||
return !!neoforge_versions?.value.gameVersions.some((x) => item.version === x.id)
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
})
|
||||
|
||||
const hasSnapshots = computed(() =>
|
||||
gameVersionsForLoader.value?.some((x) => x.version_type !== 'release'),
|
||||
)
|
||||
|
||||
const selectableGameVersionNumbers = computed(() => {
|
||||
return gameVersionsForLoader.value
|
||||
?.filter((x) => x.version_type === 'release' || showSnapshots.value)
|
||||
.map((x) => x.version)
|
||||
})
|
||||
|
||||
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = 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<number> = ref(-1)
|
||||
|
||||
resetLoaderVersionIndex()
|
||||
|
||||
function resetLoaderVersionIndex() {
|
||||
loaderVersionIndex.value =
|
||||
selectableLoaderVersions.value?.findIndex((x) => x.id === props.instance.loader_version) ?? -1
|
||||
}
|
||||
|
||||
const isValid = computed(() => {
|
||||
return (
|
||||
selectableGameVersionNumbers.value?.includes(gameVersion.value) &&
|
||||
((loaderVersionIndex.value !== undefined && loaderVersionIndex.value >= 0) ||
|
||||
loader.value === 'vanilla')
|
||||
)
|
||||
})
|
||||
|
||||
const isChanged = computed(() => {
|
||||
return (
|
||||
loader.value !== props.instance.loader ||
|
||||
gameVersion.value !== props.instance.game_version ||
|
||||
(loader.value !== 'vanilla' &&
|
||||
loaderVersionIndex.value !== undefined &&
|
||||
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: { loader?: string; game_version?: string; loader_version?: string } = {}
|
||||
editProfile.loader = loader.value
|
||||
editProfile.game_version = gameVersion.value
|
||||
|
||||
if (loader.value !== 'vanilla' && loaderVersionIndex.value !== undefined) {
|
||||
editProfile.loader_version = selectableLoaderVersions.value?.[loaderVersionIndex.value].id
|
||||
} else {
|
||||
loaderVersionIndex.value = -1
|
||||
}
|
||||
console.log('Editing:')
|
||||
console.log(loader.value)
|
||||
|
||||
await edit(props.instance.path, editProfile).catch(handleError)
|
||||
await repairProfile(false)
|
||||
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const repairing = ref(false)
|
||||
const reinstalling = ref(false)
|
||||
const changingVersion = ref(false)
|
||||
|
||||
async function repairProfile(force: boolean) {
|
||||
if (force) {
|
||||
repairing.value = true
|
||||
}
|
||||
await install(props.instance.path, force).catch(handleError)
|
||||
if (force) {
|
||||
repairing.value = false
|
||||
}
|
||||
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
async function unpairProfile() {
|
||||
await edit(props.instance.path, {
|
||||
linked_data: null,
|
||||
})
|
||||
modpackProject.value = null
|
||||
modpackVersion.value = null
|
||||
modpackVersions.value = null
|
||||
modalConfirmUnpair.value.hide()
|
||||
}
|
||||
|
||||
async function repairModpack() {
|
||||
reinstalling.value = true
|
||||
await update_repair_modrinth(props.instance.path).catch(handleError)
|
||||
reinstalling.value = false
|
||||
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
cannotWhileInstalling: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.cannot-while-installing',
|
||||
defaultMessage: 'Cannot {action} while installing',
|
||||
},
|
||||
cannotWhileOffline: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.cannot-while-offline',
|
||||
defaultMessage: 'Cannot {action} while offline',
|
||||
},
|
||||
cannotWhileRepairing: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.cannot-while-repairing',
|
||||
defaultMessage: 'Cannot {action} while repairing',
|
||||
},
|
||||
currentlyInstalled: {
|
||||
id: 'instance.settings.tabs.installation.currently-installed',
|
||||
defaultMessage: 'Currently installed',
|
||||
},
|
||||
platform: {
|
||||
id: 'instance.settings.tabs.installation.platform',
|
||||
defaultMessage: 'Platform',
|
||||
},
|
||||
gameVersion: {
|
||||
id: 'instance.settings.tabs.installation.game-version',
|
||||
defaultMessage: 'Game version',
|
||||
},
|
||||
loaderVersion: {
|
||||
id: 'instance.settings.tabs.installation.loader-version',
|
||||
defaultMessage: '{loader} version',
|
||||
},
|
||||
showAllVersions: {
|
||||
id: 'instance.settings.tabs.installation.show-all-versions',
|
||||
defaultMessage: 'Show all versions',
|
||||
},
|
||||
install: {
|
||||
id: 'instance.settings.tabs.installation.install',
|
||||
defaultMessage: 'Install',
|
||||
},
|
||||
resetSelections: {
|
||||
id: 'instance.settings.tabs.installation.reset-selections',
|
||||
defaultMessage: 'Reset to current',
|
||||
},
|
||||
unknownVersion: {
|
||||
id: 'instance.settings.tabs.installation.unknown-version',
|
||||
defaultMessage: '(unknown version)',
|
||||
},
|
||||
repairConfirmTitle: {
|
||||
id: 'instance.settings.tabs.installation.repair.confirm.title',
|
||||
defaultMessage: 'Repair instance?',
|
||||
},
|
||||
repairConfirmDescription: {
|
||||
id: 'instance.settings.tabs.installation.repair.confirm.description',
|
||||
defaultMessage:
|
||||
'Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods.',
|
||||
},
|
||||
repairButton: {
|
||||
id: 'instance.settings.tabs.installation.repair.button',
|
||||
defaultMessage: 'Repair',
|
||||
},
|
||||
repairingButton: {
|
||||
id: 'instance.settings.tabs.installation.repair.button.repairing',
|
||||
defaultMessage: 'Repairing',
|
||||
},
|
||||
repairInProgress: {
|
||||
id: 'instance.settings.tabs.installation.repair.in-progress',
|
||||
defaultMessage: 'Repair in progress',
|
||||
},
|
||||
repairAction: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.action.repair',
|
||||
defaultMessage: 'repair',
|
||||
},
|
||||
changeVersionCannotWhileFetching: {
|
||||
id: 'instance.settings.tabs.installation.change-version.cannot-while-fetching',
|
||||
defaultMessage: 'Fetching modpack versions',
|
||||
},
|
||||
changeVersionButton: {
|
||||
id: 'instance.settings.tabs.installation.change-version.button',
|
||||
defaultMessage: 'Change version',
|
||||
},
|
||||
changeVersionAction: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.action.change-version',
|
||||
defaultMessage: 'change version',
|
||||
},
|
||||
installingButton: {
|
||||
id: 'instance.settings.tabs.installation.change-version.button.installing',
|
||||
defaultMessage: 'Installing',
|
||||
},
|
||||
installInProgress: {
|
||||
id: 'instance.settings.tabs.installation.install.in-progress',
|
||||
defaultMessage: 'Installation in progress',
|
||||
},
|
||||
installButton: {
|
||||
id: 'instance.settings.tabs.installation.change-version.button.install',
|
||||
defaultMessage: 'Install',
|
||||
},
|
||||
alreadyInstalledVanilla: {
|
||||
id: 'instance.settings.tabs.installation.change-version.already-installed.vanilla',
|
||||
defaultMessage: 'Vanilla {game_version} already installed',
|
||||
},
|
||||
alreadyInstalledModded: {
|
||||
id: 'instance.settings.tabs.installation.change-version.already-installed.modded',
|
||||
defaultMessage: '{platform} {version} for Minecraft {game_version} already installed',
|
||||
},
|
||||
installAction: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.action.install',
|
||||
defaultMessage: 'install',
|
||||
},
|
||||
installingNewVersion: {
|
||||
id: 'instance.settings.tabs.installation.change-version.in-progress',
|
||||
defaultMessage: 'Installing new version',
|
||||
},
|
||||
minecraftVersion: {
|
||||
id: 'instance.settings.tabs.installation.minecraft-version',
|
||||
defaultMessage: 'Minecraft {version}',
|
||||
},
|
||||
noLoaderVersions: {
|
||||
id: 'instance.settings.tabs.installation.no-loader-versions',
|
||||
defaultMessage: '{loader} is not available for Minecraft {version}. Try another mod loader.',
|
||||
},
|
||||
noConnection: {
|
||||
id: 'instance.settings.tabs.installation.no-connection',
|
||||
defaultMessage: 'Cannot fetch linked modpack details. Please check your internet connection.',
|
||||
},
|
||||
noModpackFound: {
|
||||
id: 'instance.settings.tabs.installation.no-modpack-found',
|
||||
defaultMessage:
|
||||
'This instance is linked to a modpack, but the modpack could not be found on Modrinth.',
|
||||
},
|
||||
debugInformation: {
|
||||
id: 'instance.settings.tabs.installation.debug-information',
|
||||
defaultMessage: 'Debug information:',
|
||||
},
|
||||
fetchingModpackDetails: {
|
||||
id: 'instance.settings.tabs.installation.fetching-modpack-details',
|
||||
defaultMessage: 'Fetching modpack details',
|
||||
},
|
||||
unlinkInstanceTitle: {
|
||||
id: 'instance.settings.tabs.installation.unlink.title',
|
||||
defaultMessage: 'Unlink from modpack',
|
||||
},
|
||||
unlinkInstanceDescription: {
|
||||
id: 'instance.settings.tabs.installation.unlink.description',
|
||||
defaultMessage: `This instance is linked to a modpack, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the modpack.`,
|
||||
},
|
||||
unlinkInstanceButton: {
|
||||
id: 'instance.settings.tabs.installation.unlink.button',
|
||||
defaultMessage: 'Unlink instance',
|
||||
},
|
||||
unlinkInstanceConfirmTitle: {
|
||||
id: 'instance.settings.tabs.installation.unlink.confirm.title',
|
||||
defaultMessage: 'Are you sure you want to unlink this instance?',
|
||||
},
|
||||
unlinkInstanceConfirmDescription: {
|
||||
id: 'instance.settings.tabs.installation.unlink.confirm.description',
|
||||
defaultMessage:
|
||||
'If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal.',
|
||||
},
|
||||
reinstallModpackConfirmTitle: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.confirm.title',
|
||||
defaultMessage: 'Are you sure you want to reinstall this instance?',
|
||||
},
|
||||
reinstallModpackConfirmDescription: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.confirm.description',
|
||||
defaultMessage: `Reinstalling will reset all installed or modified content to what is provided by the modpack, removing any mods or content you have added on top of the original installation. This may fix unexpected behavior if changes have been made to the instance, but if your worlds now depend on additional installed content, it may break existing worlds.`,
|
||||
},
|
||||
reinstallModpackTitle: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.title',
|
||||
defaultMessage: 'Reinstall modpack',
|
||||
},
|
||||
reinstallModpackDescription: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.description',
|
||||
defaultMessage: `Resets the instance's content to its original state, removing any mods or content you have added on top of the original modpack.`,
|
||||
},
|
||||
reinstallModpackButton: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.button',
|
||||
defaultMessage: 'Reinstall modpack',
|
||||
},
|
||||
reinstallingModpackButton: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.button.reinstalling',
|
||||
defaultMessage: 'Reinstalling modpack',
|
||||
},
|
||||
reinstallAction: {
|
||||
id: 'instance.settings.tabs.installation.tooltip.action.reinstall',
|
||||
defaultMessage: 'reinstall',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmModalWrapper
|
||||
ref="repairConfirmModal"
|
||||
:title="formatMessage(messages.repairConfirmTitle)"
|
||||
:description="formatMessage(messages.repairConfirmDescription)"
|
||||
:proceed-icon="HammerIcon"
|
||||
:proceed-label="formatMessage(messages.repairButton)"
|
||||
:danger="false"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="() => repairProfile(true)"
|
||||
/>
|
||||
<ModpackVersionModal
|
||||
v-if="instance.linked_data && modpackVersions"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="modpackVersions"
|
||||
@finish-install="
|
||||
() => {
|
||||
changingVersion = false
|
||||
modpackVersion =
|
||||
modpackVersions?.find(
|
||||
(version: Version) => version.id === props.instance.linked_data?.version_id,
|
||||
) ?? null
|
||||
}
|
||||
"
|
||||
/>
|
||||
<ConfirmModalWrapper
|
||||
ref="modalConfirmUnpair"
|
||||
:title="formatMessage(messages.unlinkInstanceConfirmTitle)"
|
||||
:description="formatMessage(messages.unlinkInstanceConfirmDescription)"
|
||||
:proceed-icon="UnlinkIcon"
|
||||
:proceed-label="formatMessage(messages.unlinkInstanceButton)"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="() => unpairProfile()"
|
||||
/>
|
||||
<ConfirmModalWrapper
|
||||
ref="modalConfirmReinstall"
|
||||
:title="formatMessage(messages.reinstallModpackConfirmTitle)"
|
||||
:description="formatMessage(messages.reinstallModpackConfirmDescription)"
|
||||
:proceed-icon="DownloadIcon"
|
||||
:proceed-label="formatMessage(messages.reinstallModpackButton)"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="() => repairModpack()"
|
||||
/>
|
||||
<div>
|
||||
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.currentlyInstalled) }}
|
||||
</h2>
|
||||
<div
|
||||
v-if="!modpackProject && instance.linked_data && offline && !fetching"
|
||||
class="text-secondary font-medium mb-2"
|
||||
>
|
||||
<UnplugIcon class="top-[3px] relative" /> {{ formatMessage(messages.noConnection) }}
|
||||
</div>
|
||||
<div v-else-if="!modpackProject && instance.linked_data && !fetching" class="mb-2">
|
||||
<p class="text-brand-red font-medium mt-0">
|
||||
<IssuesIcon class="top-[3px] relative" /> {{ formatMessage(messages.noModpackFound) }}
|
||||
</p>
|
||||
<p>{{ formatMessage(messages.debugInformation) }}</p>
|
||||
<div class="bg-bg p-6 rounded-2xl mt-2 text-sm text-secondary">
|
||||
{{ instance.linked_data }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 items-center justify-between p-4 bg-bg rounded-2xl">
|
||||
<div v-if="fetching" class="flex items-center gap-2 h-10">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
{{ formatMessage(messages.fetchingModpackDetails) }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex gap-2 items-center">
|
||||
<Avatar v-if="modpackProject" :src="modpackProject?.icon_url" size="40px" />
|
||||
<div
|
||||
v-else
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 [&_svg]:h-full [&_svg]:w-full"
|
||||
>
|
||||
<div v-if="!!currentLoaderIcon" class="contents" v-html="currentLoaderIcon" />
|
||||
<WrenchIcon v-else />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-center">
|
||||
<span class="font-semibold leading-none">
|
||||
{{
|
||||
modpackProject
|
||||
? modpackProject.title
|
||||
: formatMessage(messages.minecraftVersion, { version: instance.game_version })
|
||||
}}
|
||||
</span>
|
||||
<span class="text-sm text-secondary leading-none">
|
||||
{{
|
||||
modpackProject
|
||||
? modpackVersion
|
||||
? modpackVersion?.version_number
|
||||
: 'Unknown version'
|
||||
: formatCategory(instance.loader)
|
||||
}}
|
||||
<template v-if="instance.loader !== 'vanilla' && !modpackProject">
|
||||
{{ instance.loader_version || formatMessage(messages.unknownVersion) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<ButtonStyled color="orange" type="transparent" hover-color-fill="background">
|
||||
<button
|
||||
v-tooltip="
|
||||
repairing
|
||||
? formatMessage(messages.repairInProgress)
|
||||
: installing || reinstalling
|
||||
? formatMessage(messages.cannotWhileInstalling, {
|
||||
action: formatMessage(messages.repairAction),
|
||||
})
|
||||
: offline
|
||||
? formatMessage(messages.cannotWhileOffline, {
|
||||
action: formatMessage(messages.repairAction),
|
||||
})
|
||||
: null
|
||||
"
|
||||
:disabled="installing || repairing || reinstalling || offline"
|
||||
@click="repairConfirmModal.show()"
|
||||
>
|
||||
<SpinnerIcon v-if="repairing" class="animate-spin" />
|
||||
<HammerIcon v-else />
|
||||
{{
|
||||
repairing
|
||||
? formatMessage(messages.repairingButton)
|
||||
: formatMessage(messages.repairButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="modpackProject" hover-color-fill="background">
|
||||
<button
|
||||
v-tooltip="
|
||||
changingVersion
|
||||
? formatMessage(messages.installingNewVersion)
|
||||
: repairing
|
||||
? formatMessage(messages.cannotWhileRepairing, {
|
||||
action: formatMessage(messages.changeVersionAction),
|
||||
})
|
||||
: installing || reinstalling
|
||||
? formatMessage(messages.cannotWhileInstalling, {
|
||||
action: formatMessage(messages.changeVersionAction),
|
||||
})
|
||||
: fetching && !modpackVersions
|
||||
? formatMessage(messages.changeVersionCannotWhileFetching)
|
||||
: offline
|
||||
? formatMessage(messages.cannotWhileOffline, {
|
||||
action: formatMessage(messages.changeVersionAction),
|
||||
})
|
||||
: null
|
||||
"
|
||||
:disabled="
|
||||
changingVersion ||
|
||||
repairing ||
|
||||
installing ||
|
||||
reinstalling ||
|
||||
offline ||
|
||||
fetching ||
|
||||
!modpackVersions
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
changingVersion = true
|
||||
modpackVersionModal.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="changingVersion" class="animate-spin" />
|
||||
<TransferIcon v-else />
|
||||
{{
|
||||
changingVersion
|
||||
? formatMessage(messages.installingButton)
|
||||
: formatMessage(messages.changeVersionButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="!instance.linked_data || !instance.linked_data.locked">
|
||||
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.platform) }}
|
||||
</h2>
|
||||
<Chips v-if="loaders" v-model="loader" :items="loaders.map((x) => x.name)" class="mt-2" />
|
||||
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.gameVersion) }}
|
||||
</h2>
|
||||
<div class="flex flex-wrap mt-2 gap-2">
|
||||
<TeleportDropdownMenu
|
||||
v-if="selectableGameVersionNumbers !== undefined"
|
||||
v-model="gameVersion"
|
||||
:options="selectableGameVersionNumbers"
|
||||
name="Game Version Dropdown"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="hasSnapshots"
|
||||
v-model="showSnapshots"
|
||||
:label="formatMessage(messages.showAllVersions)"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="loader !== 'vanilla'">
|
||||
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
|
||||
</h2>
|
||||
<TeleportDropdownMenu
|
||||
v-if="selectableLoaderVersions"
|
||||
:model-value="selectableLoaderVersions[loaderVersionIndex]"
|
||||
:options="selectableLoaderVersions"
|
||||
:display-name="(option: ManifestLoaderVersion) => option?.id"
|
||||
name="Version selector"
|
||||
class="mt-2"
|
||||
@change="(value) => (loaderVersionIndex = value.index)"
|
||||
/>
|
||||
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
|
||||
<IssuesIcon />
|
||||
{{ formatMessage(messages.noLoaderVersions, { loader: loader, version: gameVersion }) }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="
|
||||
installing || reinstalling
|
||||
? formatMessage(messages.installInProgress)
|
||||
: !isChanged
|
||||
? formatMessage(
|
||||
loader === 'vanilla'
|
||||
? messages.alreadyInstalledVanilla
|
||||
: messages.alreadyInstalledModded,
|
||||
{
|
||||
platform: formatCategory(loader),
|
||||
version: instance.loader_version,
|
||||
game_version: gameVersion,
|
||||
},
|
||||
)
|
||||
: repairing
|
||||
? formatMessage(messages.cannotWhileRepairing, {
|
||||
action: formatMessage(messages.installAction),
|
||||
})
|
||||
: offline
|
||||
? formatMessage(messages.cannotWhileOffline, {
|
||||
action: formatMessage(messages.installAction),
|
||||
})
|
||||
: null
|
||||
"
|
||||
:disabled="!isValid || !isChanged || editing || offline || repairing"
|
||||
@click="saveGvLoaderEdits()"
|
||||
>
|
||||
<SpinnerIcon v-if="editing" class="animate-spin" />
|
||||
<DownloadIcon v-else />
|
||||
{{
|
||||
editing
|
||||
? formatMessage(messages.installingButton)
|
||||
: formatMessage(messages.installButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
:disabled="!isChanged"
|
||||
@click="
|
||||
() => {
|
||||
loader = instance.loader
|
||||
gameVersion = instance.game_version
|
||||
resetLoaderVersionIndex()
|
||||
}
|
||||
"
|
||||
>
|
||||
<UndoIcon />
|
||||
{{ formatMessage(messages.resetSelections) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="instance.linked_data && instance.linked_data.locked">
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.unlinkInstanceTitle) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.unlinkInstanceDescription) }}
|
||||
</p>
|
||||
<ButtonStyled>
|
||||
<button class="mt-2" @click="modalConfirmUnpair.show()">
|
||||
<UnlinkIcon /> {{ formatMessage(messages.unlinkInstanceButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template v-if="modpackProject">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast block mt-4">
|
||||
{{ formatMessage(messages.reinstallModpackTitle) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.reinstallModpackDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonStyled color="red" type="outlined">
|
||||
<button
|
||||
v-tooltip="
|
||||
reinstalling
|
||||
? formatMessage(messages.reinstallingModpackButton)
|
||||
: repairing
|
||||
? formatMessage(messages.cannotWhileRepairing, {
|
||||
action: formatMessage(messages.reinstallAction),
|
||||
})
|
||||
: installing
|
||||
? formatMessage(messages.cannotWhileInstalling, {
|
||||
action: formatMessage(messages.reinstallAction),
|
||||
})
|
||||
: offline
|
||||
? formatMessage(messages.cannotWhileOffline, {
|
||||
action: formatMessage(messages.reinstallAction),
|
||||
})
|
||||
: null
|
||||
"
|
||||
class="mt-2"
|
||||
:disabled="
|
||||
changingVersion ||
|
||||
repairing ||
|
||||
installing ||
|
||||
offline ||
|
||||
fetching ||
|
||||
!modpackVersions
|
||||
"
|
||||
@click="modalConfirmReinstall.show()"
|
||||
>
|
||||
<SpinnerIcon v-if="reinstalling" class="animate-spin" />
|
||||
<DownloadIcon v-else />
|
||||
{{
|
||||
reinstalling
|
||||
? formatMessage(messages.reinstallingModpackButton)
|
||||
: formatMessage(messages.reinstallModpackButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import { Checkbox, Slider } from '@modrinth/ui'
|
||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||
import { computed, readonly, ref, watch } from 'vue'
|
||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { get } from '@/helpers/settings'
|
||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||
|
||||
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 editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
java_path?: string
|
||||
extra_launch_args?: string[]
|
||||
custom_env_vars?: string[][]
|
||||
memory?: MemorySettings
|
||||
} = {}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
overrideJavaInstall,
|
||||
javaInstall,
|
||||
overrideJavaArgs,
|
||||
javaArgs,
|
||||
overrideEnvVars,
|
||||
envVars,
|
||||
overrideMemorySettings,
|
||||
memory,
|
||||
],
|
||||
async () => {
|
||||
await edit(props.instance.path, editProfileObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const messages = defineMessages({
|
||||
javaInstallation: {
|
||||
id: 'instance.settings.tabs.java.java-installation',
|
||||
defaultMessage: 'Java installation',
|
||||
},
|
||||
javaArguments: {
|
||||
id: 'instance.settings.tabs.java.java-arguments',
|
||||
defaultMessage: 'Java arguments',
|
||||
},
|
||||
javaEnvironmentVariables: {
|
||||
id: 'instance.settings.tabs.java.environment-variables',
|
||||
defaultMessage: 'Environment variables',
|
||||
},
|
||||
javaMemory: {
|
||||
id: 'instance.settings.tabs.java.java-memory',
|
||||
defaultMessage: 'Memory allocated',
|
||||
},
|
||||
hooks: {
|
||||
id: 'instance.settings.tabs.java.hooks',
|
||||
defaultMessage: 'Hooks',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaInstallation) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
||||
<template v-if="!overrideJavaInstall">
|
||||
<div class="flex my-2 items-center gap-2 font-semibold">
|
||||
<template v-if="javaInstall">
|
||||
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
||||
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
||||
</template>
|
||||
<template v-else-if="optimalJava">
|
||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||
<span
|
||||
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
||||
one below:</span
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||
<span
|
||||
>Could not automatically determine a Java installation to use. Please set one
|
||||
below:</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="javaInstall && !overrideJavaInstall"
|
||||
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
||||
>
|
||||
{{ javaInstall.path }}
|
||||
</div>
|
||||
</template>
|
||||
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaMemory) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="memory.maximum"
|
||||
:disabled="!overrideMemorySettings"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
unit="MB"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaArguments) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
||||
<input
|
||||
id="java-args"
|
||||
v-model="javaArgs"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideJavaArgs"
|
||||
type="text"
|
||||
class="w-full"
|
||||
placeholder="Enter java arguments..."
|
||||
/>
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
||||
<input
|
||||
id="env-vars"
|
||||
v-model="envVars"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideEnvVars"
|
||||
type="text"
|
||||
class="w-full"
|
||||
placeholder="Enter environmental variables..."
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { Checkbox, Toggle } from '@modrinth/ui'
|
||||
import { computed, ref, type Ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { InstanceSettingsTabProps, AppSettings } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||
|
||||
const overrideWindowSettings = ref(
|
||||
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
||||
)
|
||||
const resolution: Ref<[number, number]> = ref(
|
||||
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
||||
)
|
||||
const fullscreenSetting: Ref<boolean> = ref(
|
||||
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||
)
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
force_fullscreen?: boolean
|
||||
game_resolution?: [number, number]
|
||||
} = {}
|
||||
|
||||
if (overrideWindowSettings.value) {
|
||||
editProfile.force_fullscreen = fullscreenSetting.value
|
||||
|
||||
if (!fullscreenSetting.value) {
|
||||
editProfile.game_resolution = resolution.value
|
||||
}
|
||||
}
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
watch(
|
||||
[overrideWindowSettings, resolution, fullscreenSetting],
|
||||
async () => {
|
||||
await edit(props.instance.path, editProfileObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const messages = defineMessages({
|
||||
customWindowSettings: {
|
||||
id: 'instance.settings.tabs.window.custom-window-settings',
|
||||
defaultMessage: 'Custom window settings',
|
||||
},
|
||||
fullscreen: {
|
||||
id: 'instance.settings.tabs.window.fullscreen',
|
||||
defaultMessage: 'Fullscreen',
|
||||
},
|
||||
fullscreenDescription: {
|
||||
id: 'instance.settings.tabs.window.fullscreen.description',
|
||||
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
||||
},
|
||||
width: {
|
||||
id: 'instance.settings.tabs.window.width',
|
||||
defaultMessage: 'Width',
|
||||
},
|
||||
widthDescription: {
|
||||
id: 'instance.settings.tabs.window.width.description',
|
||||
defaultMessage: 'The width of the game window when launched.',
|
||||
},
|
||||
enterWidth: {
|
||||
id: 'instance.settings.tabs.window.width.enter',
|
||||
defaultMessage: 'Enter width...',
|
||||
},
|
||||
height: {
|
||||
id: 'instance.settings.tabs.window.height',
|
||||
defaultMessage: 'Height',
|
||||
},
|
||||
heightDescription: {
|
||||
id: 'instance.settings.tabs.window.height.description',
|
||||
defaultMessage: 'The height of the game window when launched.',
|
||||
},
|
||||
enterHeight: {
|
||||
id: 'instance.settings.tabs.window.height.enter',
|
||||
defaultMessage: 'Enter height...',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Checkbox
|
||||
v-model="overrideWindowSettings"
|
||||
:label="formatMessage(messages.customWindowSettings)"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
if (!value) {
|
||||
resolution = globalSettings.game_resolution
|
||||
fullscreenSetting = globalSettings.force_fullscreen
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="mt-2 flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.fullscreen) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.fullscreenDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="fullscreen"
|
||||
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
||||
:checked="fullscreenSetting"
|
||||
:disabled="!overrideWindowSettings"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
fullscreenSetting = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.width) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.widthDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="width"
|
||||
v-model="resolution[0]"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.enterWidth)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(messages.height) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.heightDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="height"
|
||||
v-model="resolution[1]"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.enterHeight)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
161
apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
Normal file
161
apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ReportIcon,
|
||||
ModrinthIcon,
|
||||
ShieldIcon,
|
||||
SettingsIcon,
|
||||
GaugeIcon,
|
||||
PaintBrushIcon,
|
||||
GameIcon,
|
||||
CoffeeIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { TabbedModal } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
||||
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/plugin-os'
|
||||
import { useTheming } from '@/store/state'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const devModeCounter = ref(0)
|
||||
|
||||
const developerModeEnabled = defineMessage({
|
||||
id: 'app.settings.developer-mode-enabled',
|
||||
defaultMessage: 'Developer mode enabled.',
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.appearance',
|
||||
defaultMessage: 'Appearance',
|
||||
}),
|
||||
icon: PaintBrushIcon,
|
||||
content: AppearanceSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.privacy',
|
||||
defaultMessage: 'Privacy',
|
||||
}),
|
||||
icon: ShieldIcon,
|
||||
content: PrivacySettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.java-installations',
|
||||
defaultMessage: 'Java installations',
|
||||
}),
|
||||
icon: CoffeeIcon,
|
||||
content: JavaSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.default-instance-options',
|
||||
defaultMessage: 'Default instance options',
|
||||
}),
|
||||
icon: GameIcon,
|
||||
content: DefaultInstanceSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.resource-management',
|
||||
defaultMessage: 'Resource management',
|
||||
}),
|
||||
icon: GaugeIcon,
|
||||
content: ResourceManagementSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.feature-flags',
|
||||
defaultMessage: 'Feature flags',
|
||||
}),
|
||||
icon: ReportIcon,
|
||||
content: FeatureFlagSettings,
|
||||
developerOnly: true,
|
||||
},
|
||||
]
|
||||
|
||||
const modal = ref()
|
||||
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
const isOpen = computed(() => modal.value?.isOpen)
|
||||
|
||||
defineExpose({ show, isOpen })
|
||||
|
||||
const version = await getVersion()
|
||||
const osPlatform = getOsPlatform()
|
||||
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>
|
||||
<template>
|
||||
<ModalWrapper ref="modal">
|
||||
<template #title>
|
||||
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||
<SettingsIcon /> Settings
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
||||
<template #footer>
|
||||
<div class="mt-auto text-secondary text-sm">
|
||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||
{{ formatMessage(developerModeEnabled) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
||||
@click="devModeCount"
|
||||
>
|
||||
<ModrinthIcon class="w-6 h-6" />
|
||||
</button>
|
||||
<div>
|
||||
<p class="m-0">Modrinth App {{ version }}</p>
|
||||
<p class="m-0">
|
||||
<span v-if="osPlatform === 'macos'">MacOS</span>
|
||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||
{{ osVersion }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TabbedModal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
@@ -6,7 +6,7 @@ import { useTheming } from '@/store/theme.js'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -25,10 +25,22 @@ defineProps({
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedIcon: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
danger: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showAdOnClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['proceed'])
|
||||
@@ -44,13 +56,29 @@ defineExpose({
|
||||
},
|
||||
})
|
||||
|
||||
// function onModalHide() {
|
||||
// if (props.showAdOnClose) {
|
||||
// show_ads_window()
|
||||
// }
|
||||
// }
|
||||
|
||||
function proceed() {
|
||||
emit('proceed')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmModal ref="modal" :confirmation-text="confirmationText" :has-to-type="hasToType" :title="title"
|
||||
:description="description" :proceed-label="proceedLabel" :on-hide="onModalHide"
|
||||
:noblur="!themeStore.advancedRendering" @proceed="proceed" />
|
||||
<ConfirmModal
|
||||
ref="modal"
|
||||
:confirmation-text="confirmationText"
|
||||
:has-to-type="hasToType"
|
||||
:title="title"
|
||||
:description="description"
|
||||
:proceed-icon="proceedIcon"
|
||||
:proceed-label="proceedLabel"
|
||||
:on-hide="onModalHide"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
:danger="danger"
|
||||
@proceed="proceed"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
CoffeeIcon,
|
||||
InfoIcon,
|
||||
WrenchIcon,
|
||||
MonitorIcon,
|
||||
CodeIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
||||
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
||||
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
|
||||
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'instance.settings.tabs.general',
|
||||
defaultMessage: 'General',
|
||||
}),
|
||||
icon: InfoIcon,
|
||||
content: GeneralSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'instance.settings.tabs.installation',
|
||||
defaultMessage: 'Installation',
|
||||
}),
|
||||
icon: WrenchIcon,
|
||||
content: InstallationSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'instance.settings.tabs.window',
|
||||
defaultMessage: 'Window',
|
||||
}),
|
||||
icon: MonitorIcon,
|
||||
content: WindowSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'instance.settings.tabs.java',
|
||||
defaultMessage: 'Java and memory',
|
||||
}),
|
||||
icon: CoffeeIcon,
|
||||
content: JavaSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'instance.settings.tabs.hooks',
|
||||
defaultMessage: 'Launch hooks',
|
||||
}),
|
||||
icon: CodeIcon,
|
||||
content: HooksSettings,
|
||||
},
|
||||
]
|
||||
|
||||
const modal = ref()
|
||||
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
|
||||
const titleMessage = defineMessage({
|
||||
id: 'instance.settings.title',
|
||||
defaultMessage: 'Settings',
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper ref="modal">
|
||||
<template #title>
|
||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||
size="24px"
|
||||
:tint-by="props.instance.path"
|
||||
/>
|
||||
{{ instance.name }} <ChevronRightIcon />
|
||||
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Modal } from '@modrinth/ui'
|
||||
|
||||
import { NewModal as Modal } from '@modrinth/ui'
|
||||
// import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
|
||||
const themeStore = useTheming()
|
||||
@@ -21,8 +21,11 @@ const props = defineProps({
|
||||
return () => { }
|
||||
},
|
||||
},
|
||||
showAdOnClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const modal = ref(null)
|
||||
|
||||
defineExpose({
|
||||
@@ -36,12 +39,18 @@ defineExpose({
|
||||
})
|
||||
|
||||
function onModalHide() {
|
||||
// if (props.showAdOnClose) {
|
||||
// show_ads_window()
|
||||
// }
|
||||
props.onHide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
||||
<template #title>
|
||||
<slot name="title" />
|
||||
</template>
|
||||
<slot />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { Toggle, ThemeSelector, TeleportDropdownMenu } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { watch, ref } from 'vue'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const os = ref(await getOS())
|
||||
const settings = ref(await get())
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
|
||||
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
(theme) => {
|
||||
themeStore.setThemeState(theme)
|
||||
settings.theme = theme
|
||||
}
|
||||
"
|
||||
:current-theme="settings.theme"
|
||||
:theme-options="themeStore.themeOptions"
|
||||
system-theme-color="system"
|
||||
/>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
|
||||
<p class="m-0 mt-1">
|
||||
Enables advanced rendering such as blur effects that may cause performance issues without
|
||||
hardware-accelerated rendering.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.advancedRendering"
|
||||
:checked="themeStore.advancedRendering"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.advancedRendering = e
|
||||
settings.advanced_rendering = themeStore.advancedRendering
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native Decorations</h2>
|
||||
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="native-decorations"
|
||||
:model-value="settings.native_decorations"
|
||||
:checked="settings.native_decorations"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.native_decorations = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
|
||||
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="minimize-launcher"
|
||||
:model-value="settings.hide_on_process_start"
|
||||
:checked="settings.hide_on_process_start"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.hide_on_process_start = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
|
||||
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
||||
</div>
|
||||
<TeleportDropdownMenu
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
:options="['Home', 'Library']"
|
||||
/>
|
||||
</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>
|
||||
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { Slider, Toggle } from '@modrinth/ui'
|
||||
|
||||
const fetchSettings = await get()
|
||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
|
||||
|
||||
const settings = ref(fetchSettings)
|
||||
|
||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||
|
||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||
setSettings.custom_env_vars = setSettings.envVars
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
|
||||
if (!setSettings.hooks.pre_launch) {
|
||||
setSettings.hooks.pre_launch = null
|
||||
}
|
||||
if (!setSettings.hooks.wrapper) {
|
||||
setSettings.hooks.wrapper = null
|
||||
}
|
||||
if (!setSettings.hooks.post_exit) {
|
||||
setSettings.hooks.post_exit = null
|
||||
}
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
Overwrites the options.txt file to start in full screen when launched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
id="fullscreen"
|
||||
:model-value="settings.force_fullscreen"
|
||||
:checked="settings.force_fullscreen"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.force_fullscreen = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The width of the game window when launched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="width"
|
||||
v-model="settings.game_resolution[0]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter width..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The height of the game window when launched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="height"
|
||||
v-model="settings.game_resolution[1]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
class="input"
|
||||
placeholder="Enter height..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
|
||||
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
unit="MB"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
||||
<input
|
||||
id="java-args"
|
||||
v-model="settings.launchArgs"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter java arguments..."
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
||||
<input
|
||||
id="env-vars"
|
||||
v-model="settings.envVars"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter environmental variables..."
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
|
||||
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
|
||||
<input
|
||||
id="pre-launch"
|
||||
v-model="settings.hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter pre-launch command..."
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
Wrapper command for launching Minecraft.
|
||||
</p>
|
||||
<input
|
||||
id="wrapper"
|
||||
v-model="settings.hooks.wrapper"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter wrapper command..."
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
|
||||
<input
|
||||
id="post-exit"
|
||||
v-model="settings.hooks.post_exit"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter post-exit command..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const settings = ref(await get())
|
||||
const options = ref(['project_background', 'page_path'])
|
||||
|
||||
function getStoreValue(key: string) {
|
||||
return themeStore.featureFlags[key] ?? false
|
||||
}
|
||||
|
||||
function setStoreValue(key: string, value: boolean) {
|
||||
themeStore.featureFlags[key] = value
|
||||
settings.value.feature_flags[key] = value
|
||||
}
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
||||
{{ option }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="getStoreValue(option)"
|
||||
:checked="getStoreValue(option)"
|
||||
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
|
||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||
async function updateJavaVersion(version) {
|
||||
if (version?.path === '') {
|
||||
version.path = undefined
|
||||
}
|
||||
|
||||
if (version?.path) {
|
||||
version.path = version.path.replace('java.exe', 'javaw.exe')
|
||||
}
|
||||
|
||||
await set_java_version(version).catch(handleError)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
||||
Java {{ javaVersion }} location
|
||||
</h2>
|
||||
<JavaSelector
|
||||
:id="'java-selector-' + javaVersion"
|
||||
v-model="javaVersions[javaVersion]"
|
||||
:version="javaVersion"
|
||||
@update:model-value="updateJavaVersion"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||
|
||||
const settings = ref(await get())
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
if (settings.value.telemetry) {
|
||||
optInAnalytics()
|
||||
} else {
|
||||
optOutAnalytics()
|
||||
}
|
||||
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
||||
<p class="m-0 text-sm">
|
||||
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||
option, you opt out and ads will no longer be shown based on your interests.
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="personalized-ads"
|
||||
:model-value="settings.personalized_ads"
|
||||
:checked="settings.personalized_ads"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.personalized_ads = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
||||
<p class="m-0 text-sm">
|
||||
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||
customize your experience. By disabling this option, you opt out and your data will no
|
||||
longer be collected.
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="opt-out-analytics"
|
||||
:model-value="settings.telemetry"
|
||||
:checked="settings.telemetry"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.telemetry = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
|
||||
<p class="m-0 text-sm">
|
||||
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
||||
longer show up as a game or app you are using on your Discord profile.
|
||||
</p>
|
||||
<p class="m-0 mt-2 text-sm">
|
||||
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
|
||||
as those added by mods. (app restart required to take effect)
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="disable-discord-rpc"
|
||||
v-model="settings.discord_rpc"
|
||||
:checked="settings.discord_rpc"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import { Button, Slider } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings.js'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
|
||||
const settings = ref(await get())
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
async function purgeCache() {
|
||||
await purge_cache_types([
|
||||
'project',
|
||||
'version',
|
||||
'user',
|
||||
'team',
|
||||
'organization',
|
||||
'loader_manifest',
|
||||
'minecraft_manifest',
|
||||
'categories',
|
||||
'report_types',
|
||||
'loaders',
|
||||
'game_versions',
|
||||
'donation_platforms',
|
||||
'file_update',
|
||||
'search_results',
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
async function findLauncherDir() {
|
||||
const newDir = await open({
|
||||
multiple: false,
|
||||
directory: true,
|
||||
title: 'Select a new app directory',
|
||||
})
|
||||
|
||||
if (newDir) {
|
||||
settings.value.custom_dir = newDir
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The directory where the launcher stores all of its files. Changes will be applied after
|
||||
restarting the launcher.
|
||||
</p>
|
||||
|
||||
<div class="m-1 my-2">
|
||||
<div class="iconified-input w-full">
|
||||
<BoxIcon />
|
||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||
<Button class="r-btn" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ConfirmModalWrapper
|
||||
ref="purgeCacheConfirmModal"
|
||||
title="Are you sure you want to purge the cache?"
|
||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||
:has-to-type="false"
|
||||
proceed-label="Purge cache"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
||||
app to reload data. This may slow down the app temporarily.
|
||||
</p>
|
||||
</div>
|
||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
||||
value if you have a poor internet connection. (app restart required to take effect)
|
||||
</p>
|
||||
<Slider
|
||||
id="max-downloads"
|
||||
v-model="settings.max_concurrent_downloads"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||
</p>
|
||||
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
||||
</template>
|
||||
@@ -1,368 +0,0 @@
|
||||
<script setup>
|
||||
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
|
||||
import { Button, Card, Checkbox } from '@modrinth/ui'
|
||||
import {
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
MicrosoftIcon,
|
||||
GoogleIcon,
|
||||
SteamIcon,
|
||||
GitLabIcon,
|
||||
} from '@/assets/external'
|
||||
import { login } from '@/helpers/mr_auth.js'
|
||||
import { handleError, useNotifications } from '@/store/state.js'
|
||||
import { ref } from 'vue'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
callback: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const modal = ref()
|
||||
const turnstileToken = ref()
|
||||
const widgetId = ref()
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
modal.value.show()
|
||||
|
||||
if (window.turnstile === null || !window.turnstile) {
|
||||
const script = document.createElement('script')
|
||||
script.src =
|
||||
'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback'
|
||||
script.async = true
|
||||
script.defer = true
|
||||
document.head.appendChild(script)
|
||||
|
||||
window.onloadTurnstileCallback = loadWidget
|
||||
} else {
|
||||
loadWidget()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function loadWidget() {
|
||||
widgetId.value = window.turnstile.render('#turnstile-container', {
|
||||
sitekey: '0x4AAAAAAAW3guHM6Eunbgwu',
|
||||
callback: (token) => (turnstileToken.value = token),
|
||||
expiredCallback: () => (turnstileToken.value = null),
|
||||
})
|
||||
}
|
||||
|
||||
function removeWidget() {
|
||||
if (widgetId.value) {
|
||||
window.turnstile.remove(widgetId.value)
|
||||
widgetId.value = null
|
||||
turnstileToken.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const loggingIn = ref(true)
|
||||
const twoFactorFlow = ref(null)
|
||||
const twoFactorCode = ref('')
|
||||
|
||||
const email = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const subscribe = ref(true)
|
||||
|
||||
async function signInOauth(provider) {
|
||||
const creds = await login().catch(handleSevereError)
|
||||
|
||||
if (creds && creds.type === 'two_factor_required') {
|
||||
twoFactorFlow.value = creds.flow
|
||||
} else if (creds && creds.session) {
|
||||
props.callback()
|
||||
modal.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function signIn2fa() {
|
||||
const creds = await login_2fa(twoFactorCode.value, twoFactorFlow.value).catch(handleError)
|
||||
|
||||
if (creds && creds.session) {
|
||||
props.callback()
|
||||
modal.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
const creds = await login_pass(username.value, password.value, turnstileToken.value).catch(
|
||||
handleError,
|
||||
)
|
||||
window.turnstile.reset(widgetId.value)
|
||||
|
||||
if (creds && creds.type === 'two_factor_required') {
|
||||
twoFactorFlow.value = creds.flow
|
||||
} else if (creds && creds.session) {
|
||||
props.callback()
|
||||
modal.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function createAccount() {
|
||||
if (password.value !== confirmPassword.value) {
|
||||
const notifs = useNotifications()
|
||||
notifs.addNotification({
|
||||
title: 'An error occurred',
|
||||
text: 'Passwords do not match!',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const creds = await create_account(
|
||||
username.value,
|
||||
email.value,
|
||||
password.value,
|
||||
turnstileToken.value,
|
||||
subscribe.value,
|
||||
).catch(handleError)
|
||||
window.turnstile.reset(widgetId.value)
|
||||
|
||||
if (creds && creds.session) {
|
||||
props.callback()
|
||||
modal.value.hide()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="modal" :on-hide="removeWidget">
|
||||
<Card>
|
||||
<template v-if="twoFactorFlow">
|
||||
<h1>Enter two-factor code</h1>
|
||||
<p>Please enter a two-factor code to proceed.</p>
|
||||
<input v-model="twoFactorCode" maxlength="11" type="text" placeholder="Enter code..." />
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1 v-if="loggingIn">Login to Modrinth</h1>
|
||||
<h1 v-else>Create an account</h1>
|
||||
<div class="button-grid">
|
||||
<Button class="discord" large @click="signInOauth('discord')">
|
||||
<DiscordIcon />
|
||||
Discord
|
||||
</Button>
|
||||
<Button class="github" large @click="signInOauth('github')">
|
||||
<GithubIcon />
|
||||
Github
|
||||
</Button>
|
||||
<Button class="white" large @click="signInOauth('microsoft')">
|
||||
<MicrosoftIcon />
|
||||
Microsoft
|
||||
</Button>
|
||||
<Button class="google" large @click="signInOauth('google')">
|
||||
<GoogleIcon />
|
||||
Google
|
||||
</Button>
|
||||
<Button class="white" large @click="signInOauth('steam')">
|
||||
<SteamIcon />
|
||||
Steam
|
||||
</Button>
|
||||
<Button class="gitlab" large @click="signInOauth('gitlab')">
|
||||
<GitLabIcon />
|
||||
GitLab
|
||||
</Button>
|
||||
</div>
|
||||
<div class="divider">
|
||||
<hr />
|
||||
<p>Or</p>
|
||||
</div>
|
||||
<div v-if="!loggingIn" class="iconified-input username">
|
||||
<MailIcon />
|
||||
<input v-model="email" type="text" placeholder="Email" />
|
||||
</div>
|
||||
<div class="iconified-input username">
|
||||
<UserIcon />
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
:placeholder="loggingIn ? 'Email or username' : 'Username'"
|
||||
/>
|
||||
</div>
|
||||
<div class="iconified-input" :class="{ username: !loggingIn }">
|
||||
<LockIcon />
|
||||
<input v-model="password" type="password" placeholder="Password" />
|
||||
</div>
|
||||
<div v-if="!loggingIn" class="iconified-input username">
|
||||
<LockIcon />
|
||||
<input v-model="confirmPassword" type="password" placeholder="Confirm password" />
|
||||
</div>
|
||||
<div class="turnstile">
|
||||
<div id="turnstile-container"></div>
|
||||
<div id="turnstile-container-2"></div>
|
||||
</div>
|
||||
<Checkbox
|
||||
v-if="!loggingIn"
|
||||
v-model="subscribe"
|
||||
class="subscribe-btn"
|
||||
label="Subscribe to updates about Modrinth"
|
||||
/>
|
||||
<div class="link-row">
|
||||
<a v-if="loggingIn" class="button-base" @click="loggingIn = false"> Create account </a>
|
||||
<a v-else class="button-base" @click="loggingIn = true">Sign in</a>
|
||||
<a class="button-base" href="https://modrinth.com/auth/reset-password">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<div class="button-row">
|
||||
<Button class="transparent" large>Close</Button>
|
||||
<Button v-if="twoFactorCode" color="primary" large @click="signIn2fa"> Login </Button>
|
||||
<Button
|
||||
v-else-if="loggingIn"
|
||||
color="primary"
|
||||
large
|
||||
:disabled="!turnstileToken"
|
||||
@click="signIn"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
<Button v-else color="primary" large :disabled="!turnstileToken" @click="createAccount">
|
||||
Create account
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.modal-container) {
|
||||
.modal-body {
|
||||
width: auto;
|
||||
|
||||
.content {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 25rem;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--gap-md);
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.discord {
|
||||
background-color: #5865f2;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.github {
|
||||
background-color: #8740f1;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.white {
|
||||
background-color: var(--color-contrast);
|
||||
color: var(--color-accent-contrast);
|
||||
}
|
||||
|
||||
.google {
|
||||
background-color: #4285f4;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.gitlab {
|
||||
background-color: #fc6d26;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: var(--gap-md) 0;
|
||||
|
||||
p {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--color-raised-bg);
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
width: 100%;
|
||||
border-top: 2px solid var(--color-button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-input {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-bottom: var(--gap-sm);
|
||||
}
|
||||
|
||||
.link-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: var(--gap-md) 0;
|
||||
|
||||
a {
|
||||
color: var(--color-blue);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.btn {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
padding: var(--gap-md) 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.checkbox) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.turnstile {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid var(--color-button-bg);
|
||||
height: 66px;
|
||||
margin-top: var(--gap-md);
|
||||
|
||||
iframe {
|
||||
margin: -1px;
|
||||
min-width: calc(100% + 2px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -93,3 +93,7 @@ export async function command_listener(callback) {
|
||||
export async function warning_listener(callback) {
|
||||
return await listen('warning', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function friend_listener(callback) {
|
||||
return await listen('friend', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
17
apps/app-frontend/src/helpers/friends.js
Normal file
17
apps/app-frontend/src/helpers/friends.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export async function friends() {
|
||||
return await invoke('plugin:friends|friends')
|
||||
}
|
||||
|
||||
export async function friend_statuses() {
|
||||
return await invoke('plugin:friends|friend_statuses')
|
||||
}
|
||||
|
||||
export async function add_friend(userId) {
|
||||
return await invoke('plugin:friends|add_friend', { userId })
|
||||
}
|
||||
|
||||
export async function remove_friend(userId) {
|
||||
return await invoke('plugin:friends|remove_friend', { userId })
|
||||
}
|
||||
110
apps/app-frontend/src/helpers/types.d.ts
vendored
Normal file
110
apps/app-frontend/src/helpers/types.d.ts
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { ModrinthId } from '@modrinth/utils'
|
||||
|
||||
type GameInstance = {
|
||||
path: string
|
||||
install_stage: InstallStage
|
||||
|
||||
name: string
|
||||
icon_path?: string
|
||||
|
||||
game_version: string
|
||||
loader: InstanceLoader
|
||||
loader_version?: string
|
||||
|
||||
groups: string[]
|
||||
|
||||
linked_data?: LinkedData
|
||||
|
||||
created: Date
|
||||
modified: Date
|
||||
last_played?: Date
|
||||
|
||||
submitted_time_played: number
|
||||
recent_time_played: number
|
||||
|
||||
java_path?: string
|
||||
extra_launch_args?: string[]
|
||||
custom_env_vars?: [string, string][]
|
||||
|
||||
memory?: MemorySettings
|
||||
force_fullscreen?: boolean
|
||||
game_resolution?: [number, number]
|
||||
hooks: Hooks
|
||||
}
|
||||
|
||||
type InstallStage = 'installed' | 'installing' | 'pack_installing' | 'not_installed'
|
||||
|
||||
type LinkedData = {
|
||||
project_id: ModrinthId
|
||||
version_id: ModrinthId
|
||||
|
||||
locked: boolean
|
||||
}
|
||||
|
||||
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||
|
||||
type MemorySettings = {
|
||||
maximum: number
|
||||
}
|
||||
|
||||
type WindowSize = {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
type Hooks = {
|
||||
pre_launch?: string
|
||||
wrapper?: string
|
||||
post_exit?: string
|
||||
}
|
||||
|
||||
type Manifest = {
|
||||
gameVersions: ManifestGameVersion[]
|
||||
}
|
||||
|
||||
type ManifestGameVersion = {
|
||||
id: string
|
||||
stable: boolean
|
||||
loaders: ManifestLoaderVersion[]
|
||||
}
|
||||
|
||||
type ManifestLoaderVersion = {
|
||||
id: string
|
||||
url: string
|
||||
stable: boolean
|
||||
}
|
||||
|
||||
type AppSettings = {
|
||||
max_concurrent_downloads: number
|
||||
max_concurrent_writes: number
|
||||
|
||||
theme: 'dark' | 'light' | 'oled'
|
||||
default_page: 'Home' | 'Library'
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
developer_mode: boolean
|
||||
personalized_ads: boolean
|
||||
|
||||
onboarded: boolean
|
||||
|
||||
extra_launch_args: string[]
|
||||
custom_env_vars: [string, string][]
|
||||
memory: MemorySettings
|
||||
force_fullscreen: boolean
|
||||
game_resolution: [number, number]
|
||||
hide_on_process_start: boolean
|
||||
hooks: Hooks
|
||||
|
||||
custom_dir?: string
|
||||
prev_custom_dir?: string
|
||||
migrated: boolean
|
||||
}
|
||||
|
||||
export type InstanceSettingsTabProps = {
|
||||
instance: GameInstance
|
||||
offline?: boolean
|
||||
}
|
||||
@@ -54,17 +54,3 @@ export const releaseColor = (releaseType) => {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce(fn, wait) {
|
||||
let timer
|
||||
return function (...args) {
|
||||
if (timer) {
|
||||
clearTimeout(timer) // clear any pre-existing timer
|
||||
}
|
||||
|
||||
const context = this // get the current context
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(context, args) // call the function if time expires
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
|
||||
320
apps/app-frontend/src/locales/en-US/index.json
Normal file
320
apps/app-frontend/src/locales/en-US/index.json
Normal file
@@ -0,0 +1,320 @@
|
||||
{
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Developer mode enabled."
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Appearance"
|
||||
},
|
||||
"app.settings.tabs.default-instance-options": {
|
||||
"message": "Default instance options"
|
||||
},
|
||||
"app.settings.tabs.feature-flags": {
|
||||
"message": "Feature flags"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Java installations"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Privacy"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Resource management"
|
||||
},
|
||||
"instance.filter.updates-available": {
|
||||
"message": "Updates available"
|
||||
},
|
||||
"instance.settings.tabs.general": {
|
||||
"message": "General"
|
||||
},
|
||||
"instance.settings.tabs.general.delete": {
|
||||
"message": "Delete instance"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.button": {
|
||||
"message": "Delete instance"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it."
|
||||
},
|
||||
"instance.settings.tabs.general.deleting.button": {
|
||||
"message": "Deleting..."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button": {
|
||||
"message": "Duplicate"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
|
||||
"message": "Cannot duplicate while installing."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance": {
|
||||
"message": "Duplicate instance"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "Creates a copy of this instance, including worlds, configs, mods, etc."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "Edit icon"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.remove": {
|
||||
"message": "Remove icon"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.replace": {
|
||||
"message": "Replace icon"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.select": {
|
||||
"message": "Select icon"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups": {
|
||||
"message": "Library groups"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.create": {
|
||||
"message": "Create new group"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.description": {
|
||||
"message": "Library groups allow you to organize your instances into different sections in your library."
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||
"message": "Enter group name"
|
||||
},
|
||||
"instance.settings.tabs.general.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"instance.settings.tabs.hooks": {
|
||||
"message": "Launch hooks"
|
||||
},
|
||||
"instance.settings.tabs.hooks.custom-hooks": {
|
||||
"message": "Custom launch hooks"
|
||||
},
|
||||
"instance.settings.tabs.hooks.description": {
|
||||
"message": "Hooks allow advanced users to run certain system commands before and after launching the game."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit": {
|
||||
"message": "Post-exit"
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.description": {
|
||||
"message": "Ran after the game closes."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.enter": {
|
||||
"message": "Enter post-exit command..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch": {
|
||||
"message": "Pre-launch"
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.description": {
|
||||
"message": "Ran before the instance is launched."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.enter": {
|
||||
"message": "Enter pre-launch command..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.title": {
|
||||
"message": "Game launch hooks"
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper": {
|
||||
"message": "Wrapper"
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.description": {
|
||||
"message": "Wrapper command for launching Minecraft."
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.enter": {
|
||||
"message": "Enter wrapper command..."
|
||||
},
|
||||
"instance.settings.tabs.installation": {
|
||||
"message": "Installation"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.modded": {
|
||||
"message": "{platform} {version} for Minecraft {game_version} already installed"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
|
||||
"message": "Vanilla {game_version} already installed"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button": {
|
||||
"message": "Change version"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.install": {
|
||||
"message": "Install"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.installing": {
|
||||
"message": "Installing"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
|
||||
"message": "Fetching modpack versions"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.in-progress": {
|
||||
"message": "Installing new version"
|
||||
},
|
||||
"instance.settings.tabs.installation.currently-installed": {
|
||||
"message": "Currently installed"
|
||||
},
|
||||
"instance.settings.tabs.installation.debug-information": {
|
||||
"message": "Debug information:"
|
||||
},
|
||||
"instance.settings.tabs.installation.fetching-modpack-details": {
|
||||
"message": "Fetching modpack details"
|
||||
},
|
||||
"instance.settings.tabs.installation.game-version": {
|
||||
"message": "Game version"
|
||||
},
|
||||
"instance.settings.tabs.installation.install": {
|
||||
"message": "Install"
|
||||
},
|
||||
"instance.settings.tabs.installation.install.in-progress": {
|
||||
"message": "Installation in progress"
|
||||
},
|
||||
"instance.settings.tabs.installation.loader-version": {
|
||||
"message": "{loader} version"
|
||||
},
|
||||
"instance.settings.tabs.installation.minecraft-version": {
|
||||
"message": "Minecraft {version}"
|
||||
},
|
||||
"instance.settings.tabs.installation.no-connection": {
|
||||
"message": "Cannot fetch linked modpack details. Please check your internet connection."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-loader-versions": {
|
||||
"message": "{loader} is not available for Minecraft {version}. Try another mod loader."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-modpack-found": {
|
||||
"message": "This instance is linked to a modpack, but the modpack could not be found on Modrinth."
|
||||
},
|
||||
"instance.settings.tabs.installation.platform": {
|
||||
"message": "Platform"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button": {
|
||||
"message": "Reinstall modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
|
||||
"message": "Reinstalling modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.description": {
|
||||
"message": "Reinstalling will reset all installed or modified content to what is provided by the modpack, removing any mods or content you have added on top of the original installation. This may fix unexpected behavior if changes have been made to the instance, but if your worlds now depend on additional installed content, it may break existing worlds."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.title": {
|
||||
"message": "Are you sure you want to reinstall this instance?"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.description": {
|
||||
"message": "Resets the instance's content to its original state, removing any mods or content you have added on top of the original modpack."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.title": {
|
||||
"message": "Reinstall modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button": {
|
||||
"message": "Repair"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button.repairing": {
|
||||
"message": "Repairing"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.description": {
|
||||
"message": "Repairing reinstalls Minecraft dependencies and checks for corruption. This may resolve issues if your game is not launching due to launcher-related errors, but will not resolve issues or crashes related to installed mods."
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.title": {
|
||||
"message": "Repair instance?"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.in-progress": {
|
||||
"message": "Repair in progress"
|
||||
},
|
||||
"instance.settings.tabs.installation.reset-selections": {
|
||||
"message": "Reset to current"
|
||||
},
|
||||
"instance.settings.tabs.installation.show-all-versions": {
|
||||
"message": "Show all versions"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.change-version": {
|
||||
"message": "change version"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.install": {
|
||||
"message": "install"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.reinstall": {
|
||||
"message": "reinstall"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.repair": {
|
||||
"message": "repair"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
|
||||
"message": "Cannot {action} while installing"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
|
||||
"message": "Cannot {action} while offline"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
|
||||
"message": "Cannot {action} while repairing"
|
||||
},
|
||||
"instance.settings.tabs.installation.unknown-version": {
|
||||
"message": "(unknown version)"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.button": {
|
||||
"message": "Unlink instance"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.confirm.description": {
|
||||
"message": "If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.confirm.title": {
|
||||
"message": "Are you sure you want to unlink this instance?"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.description": {
|
||||
"message": "This instance is linked to a modpack, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the modpack."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.title": {
|
||||
"message": "Unlink from modpack"
|
||||
},
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java and memory"
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Environment variables"
|
||||
},
|
||||
"instance.settings.tabs.java.hooks": {
|
||||
"message": "Hooks"
|
||||
},
|
||||
"instance.settings.tabs.java.java-arguments": {
|
||||
"message": "Java arguments"
|
||||
},
|
||||
"instance.settings.tabs.java.java-installation": {
|
||||
"message": "Java installation"
|
||||
},
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Memory allocated"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Window"
|
||||
},
|
||||
"instance.settings.tabs.window.custom-window-settings": {
|
||||
"message": "Custom window settings"
|
||||
},
|
||||
"instance.settings.tabs.window.fullscreen": {
|
||||
"message": "Fullscreen"
|
||||
},
|
||||
"instance.settings.tabs.window.fullscreen.description": {
|
||||
"message": "Make the game start in full screen when launched (using options.txt)."
|
||||
},
|
||||
"instance.settings.tabs.window.height": {
|
||||
"message": "Height"
|
||||
},
|
||||
"instance.settings.tabs.window.height.description": {
|
||||
"message": "The height of the game window when launched."
|
||||
},
|
||||
"instance.settings.tabs.window.height.enter": {
|
||||
"message": "Enter height..."
|
||||
},
|
||||
"instance.settings.tabs.window.width": {
|
||||
"message": "Width"
|
||||
},
|
||||
"instance.settings.tabs.window.width.description": {
|
||||
"message": "The width of the game window when launched."
|
||||
},
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "Enter width..."
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "Settings"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Provided by the instance"
|
||||
},
|
||||
"search.filter.locked.instance-game-version.title": {
|
||||
"message": "Game version is provided by the instance"
|
||||
},
|
||||
"search.filter.locked.instance-loader.title": {
|
||||
"message": "Loader is provided by the instance"
|
||||
},
|
||||
"search.filter.locked.instance.sync": {
|
||||
"message": "Sync with instance"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,16 @@ Sentry.init({
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(FloatingVue)
|
||||
app.use(FloatingVue, {
|
||||
themes: {
|
||||
'ribbit-popout': {
|
||||
$extend: 'dropdown',
|
||||
placement: 'bottom-end',
|
||||
instantMove: true,
|
||||
distance: 8,
|
||||
},
|
||||
},
|
||||
})
|
||||
app.use(VIntlPlugin)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,19 +31,21 @@ window.addEventListener('online', () => {
|
||||
const getInstances = async () => {
|
||||
const profiles = await list().catch(handleError)
|
||||
|
||||
recentInstances.value = profiles.sort((a, b) => {
|
||||
const dateA = dayjs(a.last_played ?? 0)
|
||||
const dateB = dayjs(b.last_played ?? 0)
|
||||
recentInstances.value = profiles
|
||||
.filter((x) => x.last_played)
|
||||
.sort((a, b) => {
|
||||
const dateA = dayjs(a.last_played)
|
||||
const dateB = dayjs(b.last_played)
|
||||
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const filters = []
|
||||
for (const instance of recentInstances.value) {
|
||||
for (const instance of profiles) {
|
||||
if (instance.linked_data && instance.linked_data.project_id) {
|
||||
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
|
||||
}
|
||||
@@ -99,37 +101,34 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<RowDisplay v-if="total > 0" :instances="[
|
||||
{
|
||||
label: 'Jump back in',
|
||||
route: '/library',
|
||||
instances: recentInstances,
|
||||
instance: true,
|
||||
downloaded: true,
|
||||
},
|
||||
{
|
||||
label: 'Popular packs',
|
||||
route: '/browse/modpack',
|
||||
instances: featuredModpacks,
|
||||
downloaded: false,
|
||||
},
|
||||
{
|
||||
label: 'Popular mods',
|
||||
route: '/browse/mod',
|
||||
instances: featuredMods,
|
||||
downloaded: false,
|
||||
},
|
||||
]" :can-paginate="true" />
|
||||
<div class="p-6 flex flex-col gap-2">
|
||||
<h1 v-if="recentInstances" class="m-0 text-2xl">Welcome back!</h1>
|
||||
<h1 v-else class="m-0 text-2xl">Welcome to AstralRinth App!</h1>
|
||||
<RowDisplay
|
||||
v-if="total > 0"
|
||||
:instances="[
|
||||
{
|
||||
label: 'Recently played',
|
||||
route: '/library',
|
||||
instances: recentInstances,
|
||||
instance: true,
|
||||
downloaded: true,
|
||||
compact: true,
|
||||
},
|
||||
{
|
||||
label: 'Discover a modpack',
|
||||
route: '/browse/modpack',
|
||||
instances: featuredModpacks,
|
||||
downloaded: false,
|
||||
},
|
||||
{
|
||||
label: 'Discover mods',
|
||||
route: '/browse/mod',
|
||||
instances: featuredMods,
|
||||
downloaded: false,
|
||||
},
|
||||
]"
|
||||
:can-paginate="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,669 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon, PirateShipIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui'
|
||||
import { handleError, useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
|
||||
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
||||
import { optOutAnalytics } from '@/helpers/analytics'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
// import { getVersion } from '@tauri-apps/api/app'
|
||||
import { get_user, purge_cache_types } from '@/helpers/cache.js'
|
||||
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
|
||||
import { version, development_build } from '../../package.json'
|
||||
import {
|
||||
getRemote,
|
||||
getBranches,
|
||||
launcherUrl,
|
||||
latestBetaCommitLink,
|
||||
latestBetaCommitTruncatedSha,
|
||||
} from '@/helpers/update.js'
|
||||
|
||||
const pageOptions = ['Home', 'Library']
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const accessSettings = async () => {
|
||||
const settings = await get()
|
||||
|
||||
settings.launchArgs = settings.extra_launch_args.join(' ')
|
||||
settings.envVars = settings.custom_env_vars.map((x) => x.join('=')).join(' ')
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
const fetchSettings = await accessSettings().catch(handleError)
|
||||
|
||||
const settings = ref(fetchSettings)
|
||||
|
||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async (oldSettings, newSettings) => {
|
||||
if (oldSettings.loaded_config_dir !== newSettings.loaded_config_dir) {
|
||||
return
|
||||
}
|
||||
|
||||
const setSettings = JSON.parse(JSON.stringify(newSettings))
|
||||
|
||||
if (setSettings.telemetry) {
|
||||
// optInAnalytics()
|
||||
} else {
|
||||
optOutAnalytics()
|
||||
}
|
||||
|
||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||
setSettings.custom_env_vars = setSettings.envVars
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
|
||||
if (!setSettings.hooks.pre_launch) {
|
||||
setSettings.hooks.pre_launch = null
|
||||
}
|
||||
if (!setSettings.hooks.wrapper) {
|
||||
setSettings.hooks.wrapper = null
|
||||
}
|
||||
if (!setSettings.hooks.post_exit) {
|
||||
setSettings.hooks.post_exit = null
|
||||
}
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||
async function updateJavaVersion(version) {
|
||||
if (version?.path === '') {
|
||||
version.path = undefined
|
||||
}
|
||||
|
||||
if (version?.path) {
|
||||
version.path = version.path.replace('java.exe', 'javaw.exe')
|
||||
}
|
||||
|
||||
await set_java_version(version).catch(handleError)
|
||||
}
|
||||
|
||||
async function fetchCredentials() {
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (creds && creds.user_id) {
|
||||
creds.user = await get_user(creds.user_id).catch(handleError)
|
||||
}
|
||||
credentials.value = creds
|
||||
}
|
||||
|
||||
const credentials = ref()
|
||||
await fetchCredentials()
|
||||
|
||||
const loginScreenModal = ref()
|
||||
|
||||
async function logOut() {
|
||||
await logout().catch(handleError)
|
||||
await fetchCredentials()
|
||||
}
|
||||
|
||||
async function signInAfter() {
|
||||
await fetchCredentials()
|
||||
}
|
||||
|
||||
async function findLauncherDir() {
|
||||
const newDir = await open({
|
||||
multiple: false,
|
||||
directory: true,
|
||||
title: 'Select a new app directory',
|
||||
})
|
||||
|
||||
if (newDir) {
|
||||
settings.value.custom_dir = newDir
|
||||
}
|
||||
}
|
||||
|
||||
async function purgeCache() {
|
||||
await purge_cache_types([
|
||||
'project',
|
||||
'version',
|
||||
'user',
|
||||
'team',
|
||||
'organization',
|
||||
'loader_manifest',
|
||||
'minecraft_manifest',
|
||||
'categories',
|
||||
'report_types',
|
||||
'loaders',
|
||||
'game_versions',
|
||||
'donation_platforms',
|
||||
'file_update',
|
||||
'search_results',
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
await getRemote(false, false)
|
||||
await getBranches()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">General settings</span>
|
||||
</h3>
|
||||
</div>
|
||||
<ModrinthLoginScreen ref="loginScreenModal" :callback="signInAfter" />
|
||||
<div class="adjacent-input">
|
||||
<label for="sign-in">
|
||||
<span class="label__title">Manage account</span>
|
||||
<span v-if="credentials" class="label__description">
|
||||
You are currently logged in as {{ credentials.user.username }}.
|
||||
</span>
|
||||
<span v-else> Sign in to your Modrinth account. </span>
|
||||
</label>
|
||||
<button v-if="credentials" id="sign-in" class="btn" @click="logOut">
|
||||
<LogOutIcon />
|
||||
Sign out
|
||||
</button>
|
||||
<button v-else id="sign-in" class="btn" @click="$refs.loginScreenModal.show()">
|
||||
<LogInIcon />
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmModalWrapper ref="purgeCacheConfirmModal" title="Are you sure you want to purge the cache?"
|
||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||
:has-to-type="false" proceed-label="Purge cache" @proceed="purgeCache" />
|
||||
<div class="adjacent-input">
|
||||
<label for="purge-cache">
|
||||
<span class="label__title">App cache</span>
|
||||
<span class="label__description">
|
||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force
|
||||
the app to reload data. <br />
|
||||
This may slow down the app temporarily.
|
||||
</span>
|
||||
</label>
|
||||
<button id="purge-cache" class="btn" @click="$refs.purgeCacheConfirmModal.show()">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
</div>
|
||||
<label for="appDir">
|
||||
<span class="label__title">App directory</span>
|
||||
<span class="label__description">
|
||||
The directory where the launcher stores all of its files. Changes will be applied after
|
||||
restarting the launcher.
|
||||
</span>
|
||||
</label>
|
||||
<div class="app-directory">
|
||||
<div class="iconified-input">
|
||||
<BoxIcon />
|
||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||
<Button class="r-btn" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Display</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="theme">
|
||||
<span class="label__title">Color theme</span>
|
||||
<span class="label__description">Change the global launcher color theme.</span>
|
||||
</label>
|
||||
<DropdownSelect id="theme" name="Theme dropdown" :options="themeStore.themeOptions"
|
||||
:default-value="settings.theme" :model-value="settings.theme" class="theme-dropdown" @change="(e) => {
|
||||
themeStore.setThemeState(e.option.toLowerCase())
|
||||
settings.theme = themeStore.selectedTheme
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="advanced-rendering">
|
||||
<span class="label__title">Advanced rendering</span>
|
||||
<span class="label__description">
|
||||
Enables advanced rendering such as blur effects that may cause performance issues
|
||||
without hardware-accelerated rendering.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="advanced-rendering" :model-value="themeStore.advancedRendering"
|
||||
:checked="themeStore.advancedRendering" @update:model-value="(e) => {
|
||||
themeStore.advancedRendering = e
|
||||
settings.advanced_rendering = themeStore.advancedRendering
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="minimize-launcher">
|
||||
<span class="label__title">Minimize launcher</span>
|
||||
<span class="label__description">Minimize the launcher when a Minecraft process starts.</span>
|
||||
</label>
|
||||
<Toggle id="minimize-launcher" :model-value="settings.hide_on_process_start"
|
||||
:checked="settings.hide_on_process_start" @update:model-value="(e) => {
|
||||
settings.hide_on_process_start = e
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div v-if="getOS() != 'MacOS'" class="adjacent-input">
|
||||
<label for="native-decorations">
|
||||
<span class="label__title">Native decorations</span>
|
||||
<span class="label__description">Use system window frame (app restart required).</span>
|
||||
</label>
|
||||
<Toggle id="native-decorations" :model-value="settings.native_decorations"
|
||||
:checked="settings.native_decorations" @update:model-value="(e) => {
|
||||
settings.native_decorations = e
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opening-page">
|
||||
<span class="label__title">Default landing page</span>
|
||||
<span class="label__description">Change the page to which the launcher opens on.</span>
|
||||
</label>
|
||||
<DropdownSelect id="opening-page" name="Opening page dropdown" :options="pageOptions"
|
||||
:default-value="settings.default_page" :model-value="settings.default_page" class="opening-page" @change="(e) => {
|
||||
settings.default_page = e.option
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Resource management</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input">
|
||||
<label for="max-downloads">
|
||||
<span class="label__title">Maximum concurrent downloads</span>
|
||||
<span class="label__description">
|
||||
The maximum amount of files the launcher can download at the same time. Set this to a
|
||||
lower value if you have a poor internet connection. (app restart required to take
|
||||
effect)
|
||||
</span>
|
||||
</label>
|
||||
<Slider id="max-downloads" v-model="settings.max_concurrent_downloads" :min="1" :max="10" :step="1" />
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input">
|
||||
<label for="max-writes">
|
||||
<span class="label__title">Maximum concurrent writes</span>
|
||||
<span class="label__description">
|
||||
The maximum amount of files the launcher can write to the disk at once. Set this to a
|
||||
lower value if you are frequently getting I/O errors. (app restart required to take
|
||||
effect)
|
||||
</span>
|
||||
</label>
|
||||
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Privacy</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opt-out-analytics">
|
||||
<span class="label__title">Personalized ads</span>
|
||||
<span class="label__description">
|
||||
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||
option, you opt out and ads will no longer be shown based on your interests.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle
|
||||
id="opt-out-analytics"
|
||||
:model-value="settings.personalized_ads"
|
||||
:checked="settings.personalized_ads"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.personalized_ads = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opt-out-analytics">
|
||||
<span class="label__title">Telemetry</span>
|
||||
<span class="label__description">
|
||||
(Always disabled by AstralRinth) • Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||
customize your experience. By disabling this option, you opt out and your data will no
|
||||
longer be collected.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="opt-out-analytics" :model-value="settings.telemetry" :disabled="!settings.telemetry" :checked="settings.telemetry"
|
||||
@update:model-value="(e) => {
|
||||
settings.telemetry = e
|
||||
}
|
||||
" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="disable-discord-rpc">
|
||||
<span class="label__title">Discord RPC</span>
|
||||
<span class="label__description">
|
||||
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to
|
||||
no longer show up as a game or app you are using on your Discord profile. This does not
|
||||
disable any instance-specific Discord Rich Presence integrations, such as those added by
|
||||
mods. (app restart required to take effect)
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" :checked="settings.discord_rpc" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Java settings</span>
|
||||
</h3>
|
||||
</div>
|
||||
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||
<label :for="'java-' + javaVersion">
|
||||
<span class="label__title">Java {{ javaVersion }} location</span>
|
||||
</label>
|
||||
<JavaSelector
|
||||
:id="'java-selector-' + javaVersion"
|
||||
v-model="javaVersions[javaVersion]"
|
||||
:version="javaVersion"
|
||||
@update:model-value="updateJavaVersion"
|
||||
/>
|
||||
</template>
|
||||
<hr class="card-divider" />
|
||||
<label for="java-args">
|
||||
<span class="label__title">Java arguments</span>
|
||||
</label>
|
||||
<input id="java-args" v-model="settings.launchArgs" autocomplete="off" type="text" class="installation-input"
|
||||
placeholder="Enter java arguments..." />
|
||||
<label for="env-vars">
|
||||
<span class="label__title">Environmental variables</span>
|
||||
</label>
|
||||
<input id="env-vars" v-model="settings.envVars" autocomplete="off" type="text" class="installation-input"
|
||||
placeholder="Enter environmental variables..." />
|
||||
<hr class="card-divider" />
|
||||
<div class="adjacent-input">
|
||||
<label for="max-memory">
|
||||
<span class="label__title">Java memory</span>
|
||||
<span class="label__description">
|
||||
The memory allocated to each instance when it is ran.
|
||||
</span>
|
||||
</label>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="8"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
unit="MB"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Hooks</span>
|
||||
</h3>
|
||||
</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="settings.hooks.pre_launch" autocomplete="off" 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="settings.hooks.wrapper" autocomplete="off" 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="settings.hooks.post_exit" autocomplete="off" type="text"
|
||||
placeholder="Enter post-exit command..." />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Window size</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="fullscreen">
|
||||
<span class="label__title">Fullscreen</span>
|
||||
<span class="label__description">
|
||||
Overwrites the options.txt file to start in full screen when launched.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle id="fullscreen" :model-value="settings.force_fullscreen" :checked="settings.force_fullscreen"
|
||||
@update:model-value="(e) => {
|
||||
settings.force_fullscreen = 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="settings.game_resolution[0]" :disabled="settings.force_fullscreen" autocomplete="off"
|
||||
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="settings.game_resolution[1]" :disabled="settings.force_fullscreen"
|
||||
autocomplete="off" type="number" class="input" placeholder="Enter height..." />
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="label inline-fix">
|
||||
<h3>
|
||||
<span class="label__title size-card-header in"
|
||||
> About
|
||||
<p v-if="development_build" class="development option">
|
||||
You are using a development version, there may be errors.
|
||||
</p>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span class="label__title inl">AstralRinth <PirateShipIcon /> Version • {{ version }}</span>
|
||||
|
||||
<span class="label__description"
|
||||
>Latest beta commit •
|
||||
<a class="github" :href="latestBetaCommitLink">{{
|
||||
latestBetaCommitTruncatedSha
|
||||
}}</a></span
|
||||
>
|
||||
<span class="label__description"
|
||||
>All latest versions always published on GitHub
|
||||
<a class="github" :href="launcherUrl">Our GitHub repository</a></span
|
||||
>
|
||||
|
||||
<span class="label__title">Update Checker</span>
|
||||
|
||||
<span class="label__description"
|
||||
>Version on remote server •
|
||||
<p id="releaseData" class="cosmic inline-fix"></p>
|
||||
</span>
|
||||
<span class="label__description"
|
||||
>Version on local device •
|
||||
<p class="cosmic inline-fix">v{{ version }}</p></span
|
||||
>
|
||||
</label>
|
||||
<div class="inline-item-group">
|
||||
<Button icon-only @click="getRemote(false, false), getBranches()">
|
||||
<UpdatedIcon /> Check for updates
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-page {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.installation-input {
|
||||
width: 100% !important;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.theme-dropdown {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.app-directory {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
|
||||
.iconified-input {
|
||||
flex-grow: 1;
|
||||
|
||||
input {
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.development {
|
||||
color: #ff6a00;
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 1.5s ease;
|
||||
}
|
||||
.development:hover,
|
||||
.development:focus,
|
||||
.development:active {
|
||||
color: #4800d3;
|
||||
text-shadow: #801313;
|
||||
}
|
||||
|
||||
.cosmic {
|
||||
color: #3e8cde;
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
.cosmic:hover,
|
||||
.cosmic:focus,
|
||||
.cosmic:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
.download {
|
||||
color: #3e8cde;
|
||||
border: none;
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
//background-color: rgba(0, 0, 0, 0.0);
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
.download:hover,
|
||||
.download:focus,
|
||||
.download:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
a.github {
|
||||
color: #3e8cde;
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
|
||||
a.github:hover,
|
||||
a.github:focus,
|
||||
a.github:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
.inline-item-group {
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.inline-fix {
|
||||
display: inline-flex;
|
||||
margin-top: -2rem;
|
||||
margin-bottom: -2rem;
|
||||
}
|
||||
|
||||
.download-modal {
|
||||
color: #3e8cde;
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
text-decoration: none;
|
||||
text-shadow:
|
||||
0 0 4px rgba(79, 173, 255, 0.5),
|
||||
0 0 8px rgba(14, 98, 204, 0.5),
|
||||
0 0 12px rgba(122, 31, 199, 0.5);
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
.download-modal:hover,
|
||||
.download-modal:focus,
|
||||
.download-modal:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
.option {
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,4 @@
|
||||
import Index from './Index.vue'
|
||||
import Browse from './Browse.vue'
|
||||
import Library from './Library.vue'
|
||||
import Settings from './Settings.vue'
|
||||
|
||||
export { Index, Browse, Library, Settings }
|
||||
export { Index, Browse }
|
||||
|
||||
@@ -1,97 +1,129 @@
|
||||
<template>
|
||||
<div class="instance-container">
|
||||
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()">
|
||||
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ instance.name }}</h2>
|
||||
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
|
||||
<div
|
||||
class="p-6 pr-2 pb-4"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||
>
|
||||
<ExportModal ref="exportModal" :instance="instance" />
|
||||
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar :src="icon" :alt="instance.name" size="96px" :tint-by="instance.path" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ instance.name }}
|
||||
</template>
|
||||
<template #summary> </template>
|
||||
<template #stats>
|
||||
<div
|
||||
class="flex items-center gap-2 font-semibold transform capitalize border-0 border-solid border-divider pr-4 md:border-r"
|
||||
>
|
||||
<GameIcon class="h-6 w-6 text-secondary" />
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</div>
|
||||
<span class="button-group">
|
||||
<Button v-if="instance.install_stage !== 'installed'" disabled class="instance-button">
|
||||
Installing...
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="playing === true"
|
||||
color="danger"
|
||||
class="instance-button"
|
||||
@click="stopInstance('InstancePage')"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
</Button>
|
||||
<Button
|
||||
<div class="flex items-center gap-2 font-semibold">
|
||||
<TimerIcon class="h-6 w-6 text-secondary" />
|
||||
<template v-if="timePlayed > 0">
|
||||
{{ timePlayedHumanized }}
|
||||
</template>
|
||||
<template v-else> Never played </template>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled v-if="instance.install_stage !== 'installed'" color="brand" size="large">
|
||||
<button disabled>Installing...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing === true" color="red" size="large">
|
||||
<button @click="stopInstance('InstancePage')">
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="playing === false && loading === false"
|
||||
color="primary"
|
||||
class="instance-button"
|
||||
@click="startInstance('InstancePage')"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<PlayIcon />
|
||||
Play
|
||||
</Button>
|
||||
<Button
|
||||
<button @click="startInstance('InstancePage')">
|
||||
<PlayIcon />
|
||||
Play
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="loading === true && playing === false"
|
||||
disabled
|
||||
class="instance-button"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
Loading...
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip="'Open instance folder'"
|
||||
class="instance-button"
|
||||
@click="showProfileInFolder(instance.path)"
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
Folder
|
||||
</Button>
|
||||
</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="pages-list">
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/`" class="btn">
|
||||
<BoxIcon />
|
||||
Content
|
||||
</RouterLink>
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/logs`" class="btn">
|
||||
<FileIcon />
|
||||
Logs
|
||||
</RouterLink>
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/options`" class="btn">
|
||||
<SettingsIcon />
|
||||
Options
|
||||
</RouterLink>
|
||||
<button disabled>Loading...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular>
|
||||
<button v-tooltip="'Instance settings'" @click="settingsModal.show()">
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => showProfileInFolder(instance.path),
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
action: () => $refs.exportModal.show(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #share-instance> <UserPlusIcon /> Share instance </template>
|
||||
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="content">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</div>
|
||||
<div class="px-6">
|
||||
<NavTabs :links="tabs" />
|
||||
</div>
|
||||
<div class="p-6 pt-4">
|
||||
<RouterView v-slot="{ Component }" :key="instance.path">
|
||||
<template v-if="Component">
|
||||
<Suspense
|
||||
:key="instance.path"
|
||||
@pending="loadingBar.startLoading()"
|
||||
@resolve="loadingBar.stopLoading()"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
></component>
|
||||
<template #fallback>
|
||||
<LoadingIndicator />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
<template #stop> <StopCircleIcon /> Stop </template>
|
||||
<template #add_content> <PlusIcon /> Add Content </template>
|
||||
<template #add_content> <PlusIcon /> Add content </template>
|
||||
<template #edit> <EditIcon /> Edit </template>
|
||||
<template #copy_path> <ClipboardCopyIcon /> Copy Path </template>
|
||||
<template #open_folder> <ClipboardCopyIcon /> Open Folder </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy Link </template>
|
||||
<template #open_link> <ClipboardCopyIcon /> Open In Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
||||
<template #open_folder> <ClipboardCopyIcon /> Open folder </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
<template #open_link> <ClipboardCopyIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_names><EditIcon />Copy names</template>
|
||||
<template #copy_slugs><HashIcon />Copy slugs</template>
|
||||
<template #copy_links><GlobeIcon />Copy Links</template>
|
||||
<template #copy_links><GlobeIcon />Copy links</template>
|
||||
<template #toggle><EditIcon />Toggle selected</template>
|
||||
<template #disable><XIcon />Disable selected</template>
|
||||
<template #enable><CheckCircleIcon />Enable selected</template>
|
||||
@@ -103,11 +135,18 @@
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, Avatar, Card } from '@modrinth/ui'
|
||||
import {
|
||||
BoxIcon,
|
||||
Avatar,
|
||||
ContentPageHeader,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
LoadingIndicator,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
UserPlusIcon,
|
||||
ServerIcon,
|
||||
PackageIcon,
|
||||
SettingsIcon,
|
||||
FileIcon,
|
||||
PlayIcon,
|
||||
StopCircleIcon,
|
||||
EditIcon,
|
||||
@@ -121,27 +160,94 @@ import {
|
||||
XIcon,
|
||||
CheckCircleIcon,
|
||||
UpdatedIcon,
|
||||
MoreVerticalIcon,
|
||||
GameIcon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { get, kill, run } from '@/helpers/profile'
|
||||
import { get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ref, onUnmounted, computed, watch } from 'vue'
|
||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const instance = ref(await get(route.params.id).catch(handleError))
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const instance = ref()
|
||||
const modrinthVersions = ref([])
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchInstance() {
|
||||
instance.value = await get(route.params.id).catch(handleError)
|
||||
|
||||
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
|
||||
get_project(instance.value.linked_data.project_id, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((project) => {
|
||||
if (project && project.versions) {
|
||||
get_version_many(project.versions, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((versions) => {
|
||||
modrinthVersions.value = versions.sort(
|
||||
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
|
||||
await fetchInstance()
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/instance')) {
|
||||
await fetchInstance()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
label: 'Content',
|
||||
href: `/instance/${encodeURIComponent(route.params.id)}`,
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
href: `/instance/${encodeURIComponent(route.params.id)}/logs`,
|
||||
},
|
||||
])
|
||||
|
||||
breadcrumbs.setName(
|
||||
'Instance',
|
||||
@@ -156,18 +262,8 @@ breadcrumbs.setContext({
|
||||
query: route.query,
|
||||
})
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const loadingBar = useLoading()
|
||||
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
const options = ref(null)
|
||||
|
||||
const startInstance = async (context) => {
|
||||
@@ -187,32 +283,6 @@ const startInstance = async (context) => {
|
||||
})
|
||||
}
|
||||
|
||||
const checkProcess = async () => {
|
||||
const runningProcesses = await get_by_profile_path(route.params.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
|
||||
// Get information on associated modrinth versions, if any
|
||||
const modrinthVersions = ref([])
|
||||
if (!offline.value && instance.value.linked_data && instance.value.linked_data.project_id) {
|
||||
get_project(instance.value.linked_data.project_id, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((project) => {
|
||||
if (project && project.versions) {
|
||||
get_version_many(project.versions, 'must_revalidate')
|
||||
.catch(handleError)
|
||||
.then((versions) => {
|
||||
modrinthVersions.value = versions.sort(
|
||||
(a, b) => dayjs(b.date_published) - dayjs(a.date_published),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await checkProcess()
|
||||
|
||||
const stopInstance = async (context) => {
|
||||
playing.value = false
|
||||
await kill(route.params.id).catch(handleError)
|
||||
@@ -276,9 +346,11 @@ const handleOptionsClick = async (args) => {
|
||||
case 'open_folder':
|
||||
await showProfileInFolder(instance.value.path)
|
||||
break
|
||||
case 'copy_path':
|
||||
await navigator.clipboard.writeText(instance.value.path)
|
||||
case 'copy_path': {
|
||||
const fullPath = await get_full_path(instance.value.path)
|
||||
await navigator.clipboard.writeText(fullPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +367,35 @@ const unlistenProfiles = await profile_listener(async (event) => {
|
||||
})
|
||||
|
||||
const unlistenProcesses = await process_listener((e) => {
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) {
|
||||
playing.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() =>
|
||||
instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||
)
|
||||
|
||||
const settingsModal = ref()
|
||||
|
||||
const timePlayed = computed(() => {
|
||||
return instance.value.recent_time_played + instance.value.submitted_time_played
|
||||
})
|
||||
|
||||
const timePlayedHumanized = computed(() => {
|
||||
const duration = dayjs.duration(timePlayed.value, 'seconds')
|
||||
const hours = Math.floor(duration.asHours())
|
||||
if (hours >= 1) {
|
||||
return hours + ' hour' + (hours > 1 ? 's' : '')
|
||||
}
|
||||
|
||||
const minutes = Math.floor(duration.asMinutes())
|
||||
if (minutes >= 1) {
|
||||
return minutes + ' minute' + (minutes > 1 ? 's' : '')
|
||||
}
|
||||
|
||||
const seconds = Math.floor(duration.asSeconds())
|
||||
return seconds + ' second' + (seconds > 1 ? 's' : '')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,974 +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="8"
|
||||
: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;
|
||||
padding: 1rem;
|
||||
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 Mods from './Mods.vue'
|
||||
import Options from './Options.vue'
|
||||
import Logs from './Logs.vue'
|
||||
|
||||
export { Index, Mods, Options, Logs }
|
||||
export { Index, Mods, Logs }
|
||||
|
||||
17
apps/app-frontend/src/pages/library/Custom.vue
Normal file
17
apps/app-frontend/src/pages/library/Custom.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
|
||||
defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay
|
||||
v-if="instances.length > 0"
|
||||
label="Instances"
|
||||
:instances="instances.filter((i) => !i.linked_data)"
|
||||
/>
|
||||
</template>
|
||||
17
apps/app-frontend/src/pages/library/Downloaded.vue
Normal file
17
apps/app-frontend/src/pages/library/Downloaded.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
|
||||
defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay
|
||||
v-if="instances.length > 0"
|
||||
label="Instances"
|
||||
:instances="instances.filter((i) => i.linked_data)"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
|
||||
import { profile_listener } from '@/helpers/events.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import { PlusIcon } from '@modrinth/assets'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { NewInstanceImage } from '@/assets/icons'
|
||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
@@ -35,17 +35,31 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
||||
<div v-else class="no-instance">
|
||||
<div class="icon">
|
||||
<NewInstanceImage />
|
||||
<div class="p-6 flex flex-col gap-3">
|
||||
<h1 class="m-0 text-2xl hidden">Library</h1>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{ label: 'All instances', href: `/library` },
|
||||
{ label: 'Downloaded', href: `/library/downloaded` },
|
||||
{ label: 'Custom', href: `/library/custom` },
|
||||
{ label: 'Shared with me', href: `/library/shared`, shown: false },
|
||||
{ label: 'Saved', href: `/library/saved`, shown: false },
|
||||
]"
|
||||
/>
|
||||
<template v-if="instances.length > 0">
|
||||
<RouterView :instances="instances" />
|
||||
</template>
|
||||
<div v-else class="no-instance">
|
||||
<div class="icon">
|
||||
<NewInstanceImage />
|
||||
</div>
|
||||
<h3>No instances found</h3>
|
||||
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
|
||||
<PlusIcon />
|
||||
Create new instance
|
||||
</Button>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</div>
|
||||
<h3>No instances found</h3>
|
||||
<Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
|
||||
<PlusIcon />
|
||||
Create new instance
|
||||
</Button>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
13
apps/app-frontend/src/pages/library/Overview.vue
Normal file
13
apps/app-frontend/src/pages/library/Overview.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
|
||||
defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
|
||||
</template>
|
||||
6
apps/app-frontend/src/pages/library/index.js
Normal file
6
apps/app-frontend/src/pages/library/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Index from './Index.vue'
|
||||
import Overview from './Overview.vue'
|
||||
import Downloaded from './Downloaded.vue'
|
||||
import Custom from './Custom.vue'
|
||||
|
||||
export { Index, Overview, Downloaded, Custom }
|
||||
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<Card>
|
||||
<div class="markdown-body" v-html="renderHighlightedString(project?.body ?? '')" />
|
||||
<ProjectPageDescription :description="project.body" />
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderHighlightedString } from '@modrinth/utils'
|
||||
import { Card } from '@modrinth/ui'
|
||||
import { Card, ProjectPageDescription } from '@modrinth/ui'
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
@@ -21,22 +20,3 @@ export default {
|
||||
name: 'Description',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.markdown-body {
|
||||
:deep(table) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
:deep(hr),
|
||||
:deep(h1),
|
||||
:deep(h2) {
|
||||
max-width: max(60rem, 90%);
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -195,7 +195,7 @@ document.addEventListener('keypress', keyListener)
|
||||
|
||||
.expanded-image-modal {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
z-index: 11;
|
||||
overflow: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@@ -1,247 +1,157 @@
|
||||
<template>
|
||||
<div class="root-container">
|
||||
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()">
|
||||
<Card v-if="instance" class="small-instance">
|
||||
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:alt="instance.name"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="small-instance_info">
|
||||
<span class="title">{{
|
||||
instance.name.length > 20 ? instance.name.substring(0, 20) + '...' : instance.name
|
||||
}}</span>
|
||||
<span>
|
||||
{{ instance.loader.charAt(0).toUpperCase() + instance.loader.slice(1) }}
|
||||
{{ instance.game_version }}
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</Card>
|
||||
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="md" :src="data.icon_url" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ data.title }}</h2>
|
||||
{{ data.description }}
|
||||
</div>
|
||||
<Categories
|
||||
class="tags"
|
||||
:categories="
|
||||
categories.filter(
|
||||
(cat) => data.categories.includes(cat.name) && cat.project_type === 'mod',
|
||||
)
|
||||
"
|
||||
type="ignored"
|
||||
<div>
|
||||
<Teleport to="#sidebar-teleport-target">
|
||||
<ProjectSidebarCompatibility
|
||||
:project="data"
|
||||
:tags="{ loaders: allLoaders, gameVersions: allGameVersions }"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
|
||||
<ProjectSidebarCreators
|
||||
:organization="null"
|
||||
:members="members"
|
||||
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
||||
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
||||
link-target="_blank"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarDetails
|
||||
:project="data"
|
||||
:has-versions="versions.length > 0"
|
||||
:link-target="`_blank`"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
</Teleport>
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||
<template v-if="data">
|
||||
<Teleport
|
||||
v-if="themeStore.featureFlags.project_background"
|
||||
to="#background-teleport-target"
|
||||
>
|
||||
<EnvironmentIndicator
|
||||
:client-side="data.client_side"
|
||||
:server-side="data.server_side"
|
||||
:type="data.project_type"
|
||||
/>
|
||||
</Categories>
|
||||
<hr class="card-divider" />
|
||||
<div class="button-group">
|
||||
<Button
|
||||
color="primary"
|
||||
class="instance-button"
|
||||
:disabled="installed === true || installing === true"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
<a
|
||||
:href="`https://modrinth.com/${data.project_type}/${data.slug}`"
|
||||
rel="external"
|
||||
class="btn"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Site
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="sidebar-card">
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ formatNumber(data.downloads) }}</strong>
|
||||
<span class="stat-label"> download<span v-if="data.downloads !== '1'">s</span></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<HeartIcon aria-hidden="true" />
|
||||
<p>
|
||||
<strong>{{ formatNumber(data.followers) }}</strong>
|
||||
<span class="stat-label"> follower<span v-if="data.followers !== '1'">s</span></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat date">
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<span
|
||||
><span class="date-label">Created </span> {{ dayjs(data.published).fromNow() }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="stat date">
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
<span
|
||||
><span class="date-label">Updated </span> {{ dayjs(data.updated).fromNow() }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="button-group">
|
||||
<Button class="instance-button" disabled>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</Button>
|
||||
<Button class="instance-button" disabled>
|
||||
<HeartIcon />
|
||||
Follow
|
||||
</Button>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="links">
|
||||
<a
|
||||
v-if="data.issues_url"
|
||||
:href="data.issues_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<IssuesIcon aria-hidden="true" />
|
||||
<span>Issues</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="data.source_url"
|
||||
:href="data.source_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<CodeIcon aria-hidden="true" />
|
||||
<span>Source</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="data.wiki_url"
|
||||
:href="data.wiki_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<WikiIcon aria-hidden="true" />
|
||||
<span>Wiki</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="data.discord_url"
|
||||
:href="data.discord_url"
|
||||
class="title"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<DiscordIcon aria-hidden="true" />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
<a
|
||||
v-for="(donation, index) in data.donation_urls"
|
||||
:key="index"
|
||||
:href="donation.url"
|
||||
rel="noopener nofollow ugc external"
|
||||
>
|
||||
<BuyMeACoffeeIcon v-if="donation.id === 'bmac'" aria-hidden="true" />
|
||||
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
|
||||
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
|
||||
<PaypalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
|
||||
<OpenCollectiveIcon v-else-if="donation.id === 'open-collective'" aria-hidden="true" />
|
||||
<HeartIcon v-else-if="donation.id === 'github'" />
|
||||
<CoinsIcon v-else />
|
||||
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
|
||||
<span v-else-if="donation.id === 'patreon'">Patreon</span>
|
||||
<span v-else-if="donation.id === 'paypal'">PayPal</span>
|
||||
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
|
||||
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
|
||||
<span v-else>Donate</span>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="data" class="content-container">
|
||||
<Card class="tabs">
|
||||
<NavRow
|
||||
v-if="data.gallery.length > 0"
|
||||
<ProjectBackgroundGradient :project="data" />
|
||||
</Teleport>
|
||||
<ProjectHeader :project="data" @contextmenu.prevent.stop="handleRightClick">
|
||||
<template #actions>
|
||||
<ButtonStyled size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installed ? `This project is already installed` : null"
|
||||
:disabled="installed || installing"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'follow',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #follow> <HeartIcon /> Follow </template>
|
||||
<template #save> <BookmarkIcon /> Save </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectHeader>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}/`,
|
||||
href: `/project/${$route.params.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: `/project/${$route.params.id}/versions`,
|
||||
href: {
|
||||
path: `/project/${$route.params.id}/versions`,
|
||||
query: { l: instance?.loader, g: instance?.game_version },
|
||||
},
|
||||
subpages: ['version'],
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/project/${$route.params.id}/gallery`,
|
||||
shown: data.gallery.length > 0,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<NavRow
|
||||
v-else
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}/`,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: `/project/${$route.params.id}/versions`,
|
||||
},
|
||||
]"
|
||||
<RouterView
|
||||
:project="data"
|
||||
:versions="versions"
|
||||
:members="members"
|
||||
:instance="instance"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
:installing="installing"
|
||||
:installed-version="installedVersion"
|
||||
/>
|
||||
</Card>
|
||||
<RouterView
|
||||
:project="data"
|
||||
:versions="versions"
|
||||
:members="members"
|
||||
:instance="instance"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
:installing="installing"
|
||||
:installed-version="installedVersion"
|
||||
/>
|
||||
</template>
|
||||
<template v-else> Project data couldn't not be loaded. </template>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BookmarkIcon,
|
||||
MoreVerticalIcon,
|
||||
DownloadIcon,
|
||||
ReportIcon,
|
||||
HeartIcon,
|
||||
UpdatedIcon,
|
||||
CalendarIcon,
|
||||
IssuesIcon,
|
||||
WikiIcon,
|
||||
CoinsIcon,
|
||||
CodeIcon,
|
||||
ExternalIcon,
|
||||
CheckIcon,
|
||||
GlobeIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import {
|
||||
BuyMeACoffeeIcon,
|
||||
DiscordIcon,
|
||||
PatreonIcon,
|
||||
PaypalIcon,
|
||||
KoFiIcon,
|
||||
OpenCollectiveIcon,
|
||||
} from '@/assets/external'
|
||||
import { get_categories } from '@/helpers/tags'
|
||||
ProjectHeader,
|
||||
ProjectSidebarCompatibility,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
ProjectSidebarLinks,
|
||||
ProjectSidebarCreators,
|
||||
ProjectSidebarDetails,
|
||||
ProjectBackgroundGradient,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
@@ -249,17 +159,20 @@ import { useRoute } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
|
||||
const options = ref(null)
|
||||
const installing = ref(false)
|
||||
const data = shallowRef(null)
|
||||
const versions = shallowRef([])
|
||||
@@ -271,6 +184,11 @@ const instanceProjects = ref(null)
|
||||
const installed = ref(false)
|
||||
const installedVersion = ref(null)
|
||||
|
||||
const [allLoaders, allGameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
|
||||
async function fetchProjectData() {
|
||||
const project = await get_project(route.params.id, 'must_revalidate').catch(handleError)
|
||||
|
||||
@@ -300,13 +218,11 @@ async function fetchProjectData() {
|
||||
|
||||
await fetchProjectData()
|
||||
|
||||
const promo = ref(null)
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/project')) {
|
||||
await fetchProjectData()
|
||||
promo.value.scroll()
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -329,28 +245,30 @@ async function install(version) {
|
||||
)
|
||||
}
|
||||
|
||||
const handleRightClick = (e) => {
|
||||
options.value.showMenu(e, data.value, [
|
||||
{ name: 'install' },
|
||||
{ type: 'divider' },
|
||||
{ name: 'open_link' },
|
||||
{ name: 'copy_link' },
|
||||
const options = ref(null)
|
||||
const handleRightClick = (event) => {
|
||||
options.value.showMenu(event, data.value, [
|
||||
{
|
||||
name: 'install',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const handleOptionsClick = (args) => {
|
||||
switch (args.option) {
|
||||
case 'install':
|
||||
install(null)
|
||||
break
|
||||
case 'open_link':
|
||||
window.__TAURI_INVOKE__('tauri', {
|
||||
__tauriModule: 'Shell',
|
||||
message: {
|
||||
cmd: 'open',
|
||||
path: `https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
},
|
||||
})
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
@@ -518,27 +436,7 @@ const handleOptionsClick = (args) => {
|
||||
}
|
||||
}
|
||||
|
||||
.small-instance {
|
||||
padding: var(--gap-lg);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--gap-md);
|
||||
|
||||
.instance {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.small-instance_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.project-sidebar-section {
|
||||
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,165 +1,82 @@
|
||||
<template>
|
||||
<Card class="filter-header">
|
||||
<div class="manage">
|
||||
<multiselect
|
||||
v-model="filterLoader"
|
||||
:options="
|
||||
versions
|
||||
.flatMap((value) => value.loaders)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => versions.length <= 6"
|
||||
placeholder="Filter loader..."
|
||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
||||
/>
|
||||
<multiselect
|
||||
v-model="filterGameVersions"
|
||||
:options="
|
||||
versions
|
||||
.flatMap((value) => value.game_versions)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => versions.length <= 6"
|
||||
placeholder="Filter versions..."
|
||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
||||
/>
|
||||
<multiselect
|
||||
v-model="filterVersions"
|
||||
:options="
|
||||
versions
|
||||
.map((value) => value.version_type)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => versions.length <= 6"
|
||||
placeholder="Filter release channel..."
|
||||
:custom-label="(option) => option.charAt(0).toUpperCase() + option.slice(1)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
class="no-wrap clear-filters"
|
||||
:disabled="
|
||||
filterVersions.length === 0 && filterLoader.length === 0 && filterGameVersions.length === 0
|
||||
"
|
||||
:action="clearFilters"
|
||||
<div>
|
||||
<ProjectPageVersions
|
||||
:loaders="loaders"
|
||||
:game-versions="gameVersions"
|
||||
:versions="versions"
|
||||
:project="project"
|
||||
:version-link="(version) => `/project/${project.id}/version/${version.id}`"
|
||||
>
|
||||
<ClearIcon />
|
||||
Clear filters
|
||||
</Button>
|
||||
</Card>
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<Card class="mod-card">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
<div class="table-cell table-text download-cell" />
|
||||
<div class="name-cell table-cell table-text">Name</div>
|
||||
<div class="table-cell table-text">Supports</div>
|
||||
<div class="table-cell table-text">Stats</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||
:key="version.id"
|
||||
class="table-row selectable"
|
||||
@click="$router.push(`/project/${$route.params.id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<Button
|
||||
:color="installed && version.id === installedVersion ? '' : 'primary'"
|
||||
icon-only
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="`Install`"
|
||||
:class="{
|
||||
'group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted':
|
||||
!installed || version.id !== installedVersion,
|
||||
}"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installed && version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="name-cell table-cell table-text">
|
||||
<div class="version-link">
|
||||
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
||||
<div class="version-badge">
|
||||
<div class="channel-indicator">
|
||||
<Badge
|
||||
:color="releaseColor(version.version_type)"
|
||||
:type="
|
||||
version.version_type.charAt(0).toUpperCase() + version.version_type.slice(1)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{{ version.version_number }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-cell table-text stacked-text">
|
||||
<span>
|
||||
{{
|
||||
version.loaders.map((str) => str.charAt(0).toUpperCase() + str.slice(1)).join(', ')
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ version.game_versions.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-cell table-text stacked-text">
|
||||
<div>
|
||||
<span> Published on </span>
|
||||
<strong>
|
||||
{{
|
||||
new Date(version.date_published).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{{ formatNumber(version.downloads) }}
|
||||
</strong>
|
||||
<span> Downloads </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="false"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:options="[
|
||||
{
|
||||
id: 'install-elsewhere',
|
||||
action: () => {},
|
||||
shown: false && !!instance,
|
||||
color: 'primary',
|
||||
hoverFilled: true,
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #install-elsewhere>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Add to another instance
|
||||
</template>
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
</OverflowMenu>
|
||||
<a
|
||||
v-else
|
||||
v-tooltip="`Open in browser`"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectPageVersions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Card, Button, Pagination, Badge } from '@modrinth/ui'
|
||||
import { CheckIcon, ClearIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ProjectPageVersions, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import { ref } from 'vue'
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -186,40 +103,10 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const filterVersions = ref([])
|
||||
const filterLoader = ref(props.instance ? [props.instance?.loader] : [])
|
||||
const filterGameVersions = ref(props.instance ? [props.instance?.game_version] : [])
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
const clearFilters = () => {
|
||||
filterVersions.value = []
|
||||
filterLoader.value = []
|
||||
filterGameVersions.value = []
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
return props.versions.filter(
|
||||
(projectVersion) =>
|
||||
(filterGameVersions.value.length === 0 ||
|
||||
filterGameVersions.value.some((gameVersion) =>
|
||||
projectVersion.game_versions.includes(gameVersion),
|
||||
)) &&
|
||||
(filterLoader.value.length === 0 ||
|
||||
filterLoader.value.some((loader) => projectVersion.loaders.includes(loader))) &&
|
||||
(filterVersions.value.length === 0 ||
|
||||
filterVersions.value.includes(projectVersion.version_type)),
|
||||
)
|
||||
})
|
||||
|
||||
function switchPage(page) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
//watch all the filters and if a value changes, reset to page 1
|
||||
watch([filterVersions, filterLoader, filterGameVersions], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
const [loaders, gameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import * as Pages from '@/pages'
|
||||
import * as Project from '@/pages/project'
|
||||
import * as Instance from '@/pages/instance'
|
||||
import * as Library from '@/pages/library'
|
||||
|
||||
/**
|
||||
* Configures application routing. Add page to pages/index and then add to route table here.
|
||||
@@ -19,27 +20,36 @@ export default new createRouter({
|
||||
},
|
||||
{
|
||||
path: '/browse/:projectType',
|
||||
name: 'Browse',
|
||||
name: 'Discover content',
|
||||
component: Pages.Browse,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Browse' }],
|
||||
breadcrumb: [{ name: 'Discover content' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/library',
|
||||
name: 'Library',
|
||||
component: Pages.Library,
|
||||
component: Library.Index,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Library' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: Pages.Settings,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Settings' }],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Overview',
|
||||
component: Library.Overview,
|
||||
},
|
||||
{
|
||||
path: 'downloaded',
|
||||
name: 'Downloaded',
|
||||
component: Library.Downloaded,
|
||||
},
|
||||
{
|
||||
path: 'custom',
|
||||
name: 'Custom',
|
||||
component: Library.Custom,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/project/:id',
|
||||
@@ -114,15 +124,6 @@ export default new createRouter({
|
||||
breadcrumb: [{ name: '?Instance' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'options',
|
||||
name: 'Options',
|
||||
component: Instance.Options,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Options' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
name: 'Logs',
|
||||
@@ -139,9 +140,9 @@ export default new createRouter({
|
||||
linkExactActiveClass: 'router-link-exact-active',
|
||||
scrollBehavior() {
|
||||
// Sometimes Vue's scroll behavior is not working as expected, so we need to manually scroll to top (especially on Linux)
|
||||
document.querySelector('.router-view')?.scrollTo(0, 0)
|
||||
document.querySelector('.app-viewport')?.scrollTo(0, 0)
|
||||
return {
|
||||
el: '.router-view',
|
||||
el: '.app-viewport',
|
||||
top: 0,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,9 +2,13 @@ import { defineStore } from 'pinia'
|
||||
|
||||
export const useTheming = defineStore('themeStore', {
|
||||
state: () => ({
|
||||
themeOptions: ['dark', 'light', 'oled'],
|
||||
themeOptions: ['dark', 'light', 'oled', 'system'],
|
||||
advancedRendering: true,
|
||||
selectedTheme: 'dark',
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: {},
|
||||
}),
|
||||
actions: {
|
||||
setThemeState(newTheme) {
|
||||
@@ -17,7 +21,18 @@ export const useTheming = defineStore('themeStore', {
|
||||
for (const theme of this.themeOptions) {
|
||||
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`)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ export default {
|
||||
'./src/layouts/**/*.vue',
|
||||
'./src/pages/**/*.vue',
|
||||
'./src/plugins/**/*.{js,ts}',
|
||||
'./src/app.vue',
|
||||
'./src/App.vue',
|
||||
'./src/error.vue',
|
||||
// monorepo - TODO: migrate this to its own package
|
||||
'../../packages/**/*.{js,vue,ts}',
|
||||
@@ -13,9 +13,9 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
icon: 'var(--color-icon)',
|
||||
icon: 'var(--color-base)',
|
||||
// Text
|
||||
primary: 'var(--color-text)',
|
||||
primary: 'var(--color-base)',
|
||||
contrast: 'var(--color-contrast)',
|
||||
secondary: 'var(--color-secondary)',
|
||||
inactive: 'var(--color-text-inactive)',
|
||||
@@ -65,6 +65,9 @@ export default {
|
||||
textHover: 'var(--color-button-text-hover)',
|
||||
bgActive: 'var(--color-button-bg-active)',
|
||||
textActive: 'var(--color-button-text-active)',
|
||||
border: 'var(--color-button-border)',
|
||||
bgSelected: 'var(--color-button-bg-selected)',
|
||||
textSelected: 'var(--color-button-text-selected)',
|
||||
},
|
||||
toggleHandle: 'var(--color-toggle-handle)',
|
||||
dropdown: {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
HTML Testing playground for Theseus:
|
||||
|
||||
<br><br><a href="modrinth://mod/test_id">Install mod 'test_id'</a>
|
||||
<br /><br /><a href="modrinth://mod/test_id">Install mod 'test_id'</a>
|
||||
|
||||
2
apps/app/.gitignore
vendored
2
apps/app/.gitignore
vendored
@@ -2,3 +2,5 @@
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by tauri, metadata generated at compile time
|
||||
/gen/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.8.9"
|
||||
version = "0.9.2"
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
@@ -8,22 +8,23 @@ edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc", features = ["codegen"] }
|
||||
tauri-build = { version = "2.0.3", features = ["codegen"] }
|
||||
|
||||
[dependencies]
|
||||
theseus = { path = "../../packages/app-lib", features = ["tauri"] }
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_with = "3.0.0"
|
||||
|
||||
tauri = { version = "2.0.0-rc", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||
tauri-plugin-window-state = "2.0.0-rc"
|
||||
tauri-plugin-deep-link = "2.0.0-rc"
|
||||
tauri-plugin-os = "2.0.0-rc"
|
||||
tauri-plugin-shell = "2.0.0-rc"
|
||||
tauri-plugin-dialog = "2.0.0-rc"
|
||||
tauri-plugin-updater = { version = "2.0.0-rc" }
|
||||
tauri-plugin-single-instance = { version = "2.0.0-rc" }
|
||||
tauri = { version = "2.1.1", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||
tauri-plugin-window-state = "2.2.0"
|
||||
tauri-plugin-deep-link = "2.2.0"
|
||||
tauri-plugin-os = "2.2.0"
|
||||
tauri-plugin-opener = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-updater = { version = "2.3.0" }
|
||||
tauri-plugin-single-instance = { version = "2.2.0" }
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
@@ -59,7 +60,7 @@ objc = "0.2.7"
|
||||
rand = "0.8.5"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tauri-plugin-updater = { version = "2.0.0-rc", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
|
||||
tauri-plugin-updater = { version = "2.3.0", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
@@ -213,6 +213,19 @@ fn main() {
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"friends",
|
||||
InlinedPlugin::new()
|
||||
.commands(&[
|
||||
"friends",
|
||||
"friend_statuses",
|
||||
"add_friend",
|
||||
"remove_friend",
|
||||
])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
),
|
||||
)
|
||||
.expect("Failed to run tauri-build");
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"identifier": "core",
|
||||
"description": "",
|
||||
"local": true,
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:path:default",
|
||||
@@ -27,4 +25,4 @@
|
||||
"core:window:allow-start-dragging",
|
||||
"core:webview:allow-set-webview-zoom"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"permissions": [
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-confirm",
|
||||
"shell:allow-open",
|
||||
"opener:default",
|
||||
"os:allow-platform",
|
||||
"os:allow-version",
|
||||
"os:allow-os-type",
|
||||
@@ -33,6 +33,7 @@
|
||||
"cache:default",
|
||||
"settings:default",
|
||||
"tags:default",
|
||||
"utils:default"
|
||||
"utils:default",
|
||||
"friends:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
"identifier": "updater",
|
||||
"description": "",
|
||||
"local": true,
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"updater:default"
|
||||
]
|
||||
"windows": ["main"],
|
||||
"permissions": ["updater:default"]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default"]},"updater":{"identifier":"updater","description":"","local":true,"windows":["main"],"permissions":["updater:default"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,11 @@
|
||||
"fix": "cargo fmt && cargo clippy --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.0.0-rc.16"
|
||||
"@tauri-apps/cli": "2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/app-frontend": "workspace:*",
|
||||
"@modrinth/app-lib": "workspace:*",
|
||||
"@modrinth/daedalus": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
apps/app/src/api/friends.rs
Normal file
33
apps/app/src/api/friends.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use theseus::prelude::{UserFriend, UserStatus};
|
||||
|
||||
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("friends")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
friends,
|
||||
friend_statuses,
|
||||
add_friend,
|
||||
remove_friend
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn friends() -> crate::api::Result<Vec<UserFriend>> {
|
||||
Ok(theseus::friends::friends().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn friend_statuses() -> crate::api::Result<Vec<UserStatus>> {
|
||||
Ok(theseus::friends::friend_statuses().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_friend(user_id: &str) -> crate::api::Result<()> {
|
||||
Ok(theseus::friends::add_friend(user_id).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_friend(user_id: &str) -> crate::api::Result<()> {
|
||||
Ok(theseus::friends::remove_friend(user_id).await?)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ pub mod tags;
|
||||
pub mod utils;
|
||||
|
||||
pub mod cache;
|
||||
pub mod friends;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
||||
|
||||
|
||||
@@ -282,19 +282,59 @@ pub struct EditProfile {
|
||||
|
||||
pub game_version: Option<String>,
|
||||
pub loader: Option<ModLoader>,
|
||||
pub loader_version: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub loader_version: Option<Option<String>>,
|
||||
|
||||
pub groups: Option<Vec<String>>,
|
||||
|
||||
pub linked_data: Option<LinkedData>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub linked_data: Option<Option<LinkedData>>,
|
||||
|
||||
pub java_path: Option<String>,
|
||||
pub extra_launch_args: Option<Vec<String>>,
|
||||
pub custom_env_vars: Option<Vec<(String, String)>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub java_path: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub extra_launch_args: Option<Option<Vec<String>>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub custom_env_vars: Option<Option<Vec<(String, String)>>>,
|
||||
|
||||
pub memory: Option<MemorySettings>,
|
||||
pub force_fullscreen: Option<bool>,
|
||||
pub game_resolution: Option<WindowSize>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub memory: Option<Option<MemorySettings>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub force_fullscreen: Option<Option<bool>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub game_resolution: Option<Option<WindowSize>>,
|
||||
pub hooks: Option<Hooks>,
|
||||
}
|
||||
|
||||
@@ -312,28 +352,40 @@ pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
|
||||
if let Some(loader) = edit_profile.loader {
|
||||
prof.loader = loader;
|
||||
}
|
||||
prof.loader_version.clone_from(&edit_profile.loader_version);
|
||||
prof.linked_data.clone_from(&edit_profile.linked_data);
|
||||
|
||||
if let Some(loader_version) = edit_profile.loader_version.clone() {
|
||||
prof.loader_version = loader_version;
|
||||
}
|
||||
if let Some(linked_data) = edit_profile.linked_data.clone() {
|
||||
prof.linked_data = linked_data;
|
||||
}
|
||||
if let Some(groups) = edit_profile.groups.clone() {
|
||||
prof.groups = groups;
|
||||
}
|
||||
|
||||
prof.java_path.clone_from(&edit_profile.java_path);
|
||||
prof.memory = edit_profile.memory;
|
||||
prof.game_resolution = edit_profile.game_resolution;
|
||||
prof.force_fullscreen = edit_profile.force_fullscreen;
|
||||
|
||||
if let Some(java_path) = edit_profile.java_path.clone() {
|
||||
prof.java_path = java_path;
|
||||
}
|
||||
if let Some(memory) = edit_profile.memory {
|
||||
prof.memory = memory;
|
||||
}
|
||||
if let Some(game_resolution) = edit_profile.game_resolution {
|
||||
prof.game_resolution = game_resolution;
|
||||
}
|
||||
if let Some(force_fullscreen) = edit_profile.force_fullscreen {
|
||||
prof.force_fullscreen = force_fullscreen;
|
||||
}
|
||||
if let Some(hooks) = edit_profile.hooks.clone() {
|
||||
prof.hooks = hooks;
|
||||
}
|
||||
|
||||
prof.modified = chrono::Utc::now();
|
||||
|
||||
prof.custom_env_vars
|
||||
.clone_from(&edit_profile.custom_env_vars);
|
||||
prof.extra_launch_args
|
||||
.clone_from(&edit_profile.extra_launch_args);
|
||||
if let Some(custom_env_vars) = edit_profile.custom_env_vars.clone() {
|
||||
prof.custom_env_vars = custom_env_vars;
|
||||
}
|
||||
if let Some(extra_launch_args) = edit_profile.extra_launch_args.clone()
|
||||
{
|
||||
prof.extra_launch_args = extra_launch_args;
|
||||
}
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ use tauri::{
|
||||
}; // 0.8
|
||||
|
||||
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);
|
||||
unsafe impl Send for UnsafeWindowHandle {}
|
||||
@@ -42,7 +42,7 @@ fn position_traffic_lights(
|
||||
let title_bar_container_view = close.superview().superview();
|
||||
|
||||
let close_rect: NSRect = msg_send![close, frame];
|
||||
let button_height = close_rect.size.height;
|
||||
let button_height = close_rect.size.height + 12.0;
|
||||
|
||||
let title_bar_frame_height = button_height + y;
|
||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||
@@ -58,7 +58,7 @@ fn position_traffic_lights(
|
||||
|
||||
for (i, button) in window_buttons.into_iter().enumerate() {
|
||||
let mut rect: NSRect = NSView::frame(button);
|
||||
rect.origin.x = x + (i as f64 * space_between);
|
||||
rect.origin.x = x + (i as f64 * space_between) + 6.0;
|
||||
button.setFrameOrigin(rect.origin);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user