Switch to official launcher auth (#1118)

* Switch to official launcher auth

* add debug info

* Fix build
This commit is contained in:
Geometrically
2024-04-15 13:58:20 -07:00
committed by GitHub
parent 76447019c0
commit 2877919639
65 changed files with 1674 additions and 5349 deletions

View File

@@ -20,6 +20,7 @@ import { get } from '@/helpers/settings'
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 { handleError, useNotifications } from '@/store/notifications.js'
import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
@@ -40,15 +41,14 @@ import { TauriEvent } from '@tauri-apps/api/event'
import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
import { confirm } from '@tauri-apps/api/dialog'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
import { install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js'
const themeStore = useTheming()
const urlModal = ref(null)
const isLoading = ref(true)
const videoPlaying = ref(false)
const offline = ref(false)
const showOnboarding = ref(false)
const nativeDecorations = ref(false)
@@ -71,7 +71,6 @@ defineExpose({
} = await get()
// video should play if the user is not on linux, and has not onboarded
os.value = await getOS()
videoPlaying.value = !fully_onboarded && os.value !== 'Linux'
const dev = await isDev()
const version = await getVersion()
showOnboarding.value = !fully_onboarded
@@ -180,12 +179,19 @@ const isOnBrowse = computed(() => route.path.startsWith('/browse'))
const loading = useLoading()
const notifications = useNotifications()
const notificationsWrapper = ref(null)
const notificationsWrapper = ref()
watch(notificationsWrapper, () => {
notifications.setNotifs(notificationsWrapper.value)
})
const error = useError()
const errorModal = ref()
watch(errorModal, () => {
error.setErrorModal(errorModal.value)
})
document.querySelector('body').addEventListener('click', function (e) {
let target = e.target
while (target != null) {
@@ -245,15 +251,6 @@ command_listener(async (e) => {
</script>
<template>
<StickyTitleBar v-if="videoPlaying" />
<video
v-if="videoPlaying"
ref="onboardingVideo"
class="video"
src="@/assets/video.mp4"
autoplay
@ended="videoPlaying = false"
/>
<div v-if="failureText" class="failure dark-mode">
<div class="appbar-failure dark-mode">
<Button v-if="os != 'MacOS'" icon-only @click="TauriWindow.getCurrent().close()">
@@ -294,7 +291,7 @@ command_listener(async (e) => {
</Card>
</div>
</div>
<SplashScreen v-else-if="!videoPlaying && isLoading" app-loading />
<SplashScreen v-else-if="isLoading" app-loading />
<OnboardingScreen v-else-if="showOnboarding" :finish="() => (showOnboarding = false)" />
<div v-else class="container">
<div class="nav-container">
@@ -389,6 +386,7 @@ command_listener(async (e) => {
</div>
<URLConfirmModal ref="urlModal" />
<Notifications ref="notificationsWrapper" />
<ErrorModal ref="errorModal" />
</template>
<style lang="scss" scoped>

Binary file not shown.

View File

@@ -56,68 +56,22 @@
</Button>
</Card>
</transition>
<Modal ref="loginModal" class="modal" header="Signing in" :noblur="!themeStore.advancedRendering">
<div class="modal-body">
<QrcodeVue :value="loginUrl" class="qr-code" margin="3" size="160" />
<div class="modal-text">
<div class="label">Copy this code</div>
<div class="code-text">
<div class="code">
{{ loginCode }}
</div>
<Button
v-tooltip="'Copy code'"
icon-only
large
color="raised"
@click="() => clipboardWrite(loginCode)"
>
<ClipboardCopyIcon />
</Button>
</div>
<div>And enter it on Microsoft's website to sign in.</div>
<div class="iconified-input">
<LogInIcon />
<input type="text" :value="loginUrl" readonly />
<Button
v-tooltip="'Open link'"
icon-only
color="raised"
@click="() => clipboardWrite(loginUrl)"
>
<GlobeIcon />
</Button>
</div>
</div>
</div>
</Modal>
</template>
<script setup>
import {
Avatar,
Button,
Card,
PlusIcon,
TrashIcon,
LogInIcon,
Modal,
GlobeIcon,
ClipboardCopyIcon,
} from 'omorphia'
import { Avatar, Button, Card, PlusIcon, TrashIcon, LogInIcon } from 'omorphia'
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
import {
users,
remove_user,
authenticate_begin_flow,
authenticate_await_completion,
set_default_user,
login as login_flow,
get_default_user,
} from '@/helpers/auth'
import { get, set } from '@/helpers/settings'
import { handleError } from '@/store/state.js'
import { useTheming } from '@/store/theme.js'
import { mixpanel_track } from '@/helpers/mixpanel'
import QrcodeVue from 'qrcode.vue'
import { process_listener } from '@/helpers/events'
import { handleSevereError } from '@/store/error.js'
defineProps({
mode: {
@@ -129,16 +83,11 @@ defineProps({
const emit = defineEmits(['change'])
const loginCode = ref(null)
const themeStore = useTheming()
const settings = ref({})
const accounts = ref([])
const loginUrl = ref('')
const loginModal = ref(null)
const accounts = ref({})
const defaultUser = ref()
async function refreshValues() {
settings.value = await get().catch(handleError)
defaultUser.value = await get_default_user().catch(handleError)
accounts.value = await users().catch(handleError)
}
defineExpose({
@@ -147,46 +96,27 @@ defineExpose({
await refreshValues()
const displayAccounts = computed(() =>
accounts.value.filter((account) => settings.value.default_user !== account.id),
accounts.value.filter((account) => defaultUser.value !== account.id),
)
const selectedAccount = computed(() =>
accounts.value.find((account) => account.id === settings.value.default_user),
accounts.value.find((account) => account.id === defaultUser.value),
)
async function setAccount(account) {
settings.value.default_user = account.id
await set(settings.value).catch(handleError)
defaultUser.value = account.id
await set_default_user(account.id).catch(handleError)
emit('change')
}
const clipboardWrite = async (a) => {
navigator.clipboard.writeText(a)
}
async function login() {
const loginSuccess = await authenticate_begin_flow().catch(handleError)
loginModal.value.show()
loginCode.value = loginSuccess.user_code
loginUrl.value = loginSuccess.verification_uri
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: loginSuccess.verification_uri,
},
})
const loggedIn = await authenticate_await_completion().catch(handleError)
loginModal.value.hide()
const loggedIn = await login_flow().catch(handleSevereError)
if (loggedIn) {
await setAccount(loggedIn)
await refreshValues()
}
loginModal.value.hide()
mixpanel_track('AccountLogIn')
}

View File

@@ -0,0 +1,125 @@
<script setup>
import { Modal, XIcon } from 'omorphia'
import { ChatIcon } from '@/assets/icons'
import { ref } from 'vue'
const errorModal = ref()
const error = ref()
const title = ref('An error occurred')
const errorType = ref('unknown')
const supportLink = ref('https://support.modrinth.com')
const metadata = ref({})
defineExpose({
async show(errorVal) {
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
title.value = 'Unable to sign in to Minecraft'
errorType.value = 'minecraft_auth'
supportLink.value =
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
if (errorVal.message.includes('existing connection was forcibly closed')) {
metadata.value.network = true
}
if (errorVal.message.includes('because the target machine actively refused it')) {
metadata.value.hostsFile = true
}
} else {
title.value = 'An error occurred'
errorType.value = 'unknown'
supportLink.value = 'https://support.modrinth.com'
metadata.value = {}
}
error.value = errorVal
errorModal.value.show()
},
})
</script>
<template>
<Modal ref="errorModal" :header="title">
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
<p>
Signing into Microsoft account is a complex task for the launchers, and there are a lot
of things can go wrong.
</p>
<template v-if="metadata.network">
<h3>Network issues</h3>
<p>
It looks like there were issues with the Modrinth App connecting to Microsoft's
servers. This is often the result of a poor connection, so we recommend trying again
to see if it works. If issues continue to persist, follow the steps in
<a
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
>
our support article
</a>
to troubleshoot.
</p>
</template>
<template v-else-if="metadata.hostsFile">
<h3>Network issues</h3>
<p>
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
remote server rejected the connection. This may indicate that these services are
blocked by the hosts file. Please visit
<a
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
>
our support article
</a>
for steps on how to fix the issue.
</p>
</template>
<template v-else>
<h3>Make sure you are signing into the right Microsoft account</h3>
<p>
More often than not, this error is caused by you signing into an incorrect Microsoft
account which isn't linked to Minecraft. Double check and try again!
</p>
<h3>Try signing in and launching through the official launcher first</h3>
<p>
If you just bought Minecraft, are coming from the Bedrock Edition world and have never
played Java before, or just subscribed to PC Game Pass, you would need to start the
game at least once using the
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>.
Once you're done, come back here and sign in!
</p>
</template>
<hr />
<p>
If nothing is working and you need help, visit
<a :href="supportLink">our support page</a>
and start a chat using the widget in the bottom right and we will be more than happy to
assist!
</p>
<details>
<summary>Debug info</summary>
{{ error.message ?? error }}
</details>
</template>
<template v-else>
{{ error.message ?? error }}
</template>
</div>
<div class="input-group push-right">
<a :href="supportLink" class="btn" @click="errorModal.hide()"><ChatIcon /> Get support</a>
<button class="btn btn-primary" @click="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</Modal>
</template>
<style scoped lang="scss">
.modal-body {
display: flex;
flex-direction: column;
gap: var(--gap-md);
padding: var(--gap-lg);
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="action-groups">
<a href="https://discord.modrinth.com" class="link">
<a href="https://support.modrinth.com" class="link">
<ChatIcon />
<span> Get support </span>
</a>

View File

@@ -1,178 +0,0 @@
<template>
<div ref="button" class="button-base avatar-button" :class="{ highlighted: showDemo }">
<Avatar src="https://launcher-files.modrinth.com/assets/steve_head.png" />
</div>
<transition name="fade">
<div v-if="showDemo" class="card-section">
<Card ref="card" class="fake-account-card expanded highlighted">
<div class="selected account">
<Avatar size="xs" src="https://launcher-files.modrinth.com/assets/steve_head.png" />
<div>
<h4>Modrinth</h4>
<p>Selected</p>
</div>
<Button v-tooltip="'Log out'" icon-only color="raised">
<TrashIcon />
</Button>
</div>
<Button>
<PlusIcon />
Add account
</Button>
</Card>
<slot />
</div>
</transition>
</template>
<script setup>
import { Avatar, Button, Card, PlusIcon, TrashIcon } from 'omorphia'
defineProps({
showDemo: {
type: Boolean,
default: false,
},
})
</script>
<style scoped lang="scss">
.selected {
background: var(--color-brand-highlight);
border-radius: var(--radius-lg);
color: var(--color-contrast);
gap: 1rem;
}
.logged-out {
background: var(--color-bg);
border-radius: var(--radius-lg);
gap: 1rem;
}
.account {
width: max-content;
display: flex;
align-items: center;
text-align: left;
padding: 0.5rem 1rem;
h4,
p {
margin: 0;
}
}
.card-section {
position: absolute;
top: 0.5rem;
left: 5.5rem;
z-index: 9;
display: flex;
flex-direction: column;
}
.fake-account-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
border: 1px solid var(--color-button-bg);
width: max-content;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
&.hidden {
display: none;
}
&.isolated {
position: relative;
left: 0;
top: 0;
}
}
.accounts-title {
font-size: 1.2rem;
font-weight: bolder;
}
.account-group {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.option {
width: calc(100% - 2.25rem);
background: var(--color-raised-bg);
color: var(--color-base);
box-shadow: none;
img {
margin-right: 0.5rem;
}
}
.icon {
--size: 1.5rem !important;
}
.account-row {
display: flex;
flex-direction: row;
gap: 0.5rem;
vertical-align: center;
justify-content: space-between;
padding-right: 1rem;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.avatar-button {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--color-base);
background-color: var(--color-raised-bg);
border-radius: var(--radius-md);
width: 100%;
text-align: left;
&.expanded {
border: 1px solid var(--color-button-bg);
padding: 1rem;
}
}
.avatar-text {
margin: auto 0 auto 0.25rem;
display: flex;
flex-direction: column;
}
.text {
width: 6rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.accounts-text {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
}
</style>

View File

@@ -1,265 +0,0 @@
<template>
<div class="action-groups">
<Button color="danger" outline @click="exit">
<LogOutIcon />
Exit tutorial
</Button>
<Button v-if="showDownload" ref="infoButton" icon-only class="icon-button show-card-icon">
<DownloadIcon />
</Button>
<div v-if="showRunning" class="status highlighted">
<span class="circle running" />
<div ref="profileButton" class="running-text">Example Modpack</div>
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop">
<StopCircleIcon />
</Button>
<Button v-tooltip="'View logs'" icon-only class="icon-button">
<TerminalSquareIcon />
</Button>
</div>
<div v-else class="status">
<span class="circle stopped" />
<span class="running-text"> No running instances </span>
</div>
</div>
<transition name="download">
<div v-if="showDownload" class="info-section">
<Card ref="card" class="highlighted info-card">
<h3 class="info-title">New Modpack</h3>
<ProgressBar :progress="50" />
<div class="row">50% Downloading modpack</div>
</Card>
<slot name="download" />
</div>
</transition>
<transition name="running">
<div v-if="showRunning" class="info-section">
<slot name="running" />
</div>
</transition>
</template>
<script setup>
import {
Button,
DownloadIcon,
Card,
StopCircleIcon,
TerminalSquareIcon,
LogOutIcon,
} from 'omorphia'
import ProgressBar from '@/components/ui/ProgressBar.vue'
defineProps({
showDownload: {
type: Boolean,
default: false,
},
showRunning: {
type: Boolean,
default: false,
},
exit: {
type: Function,
required: true,
},
})
</script>
<style scoped lang="scss">
.action-groups {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
}
.arrow {
transition: transform 0.2s ease-in-out;
display: flex;
align-items: center;
&.rotate {
transform: rotate(180deg);
}
}
.status {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
border-radius: var(--radius-md);
border: 1px solid var(--color-button-bg);
padding: var(--gap-sm) var(--gap-lg);
}
.running-text {
display: flex;
flex-direction: row;
gap: var(--gap-xs);
white-space: nowrap;
overflow: hidden;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none;
&.clickable:hover {
cursor: pointer;
}
}
.circle {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
display: inline-block;
margin-right: 0.25rem;
&.running {
background-color: var(--color-brand);
}
&.stopped {
background-color: var(--color-base);
}
}
.icon-button {
background-color: rgba(0, 0, 0, 0);
box-shadow: none;
width: 1.25rem !important;
height: 1.25rem !important;
&.stop {
--text-color: var(--color-red) !important;
}
}
.info-section {
position: absolute;
top: 3.5rem;
right: 0.75rem;
z-index: 9;
display: flex;
flex-direction: column;
}
.info-card {
width: 20rem;
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-raised);
display: flex;
flex-direction: column;
gap: 1rem;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
&.hidden {
transform: translateY(-100%);
}
}
.loading-option {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0;
:hover {
background-color: var(--color-raised-bg-hover);
}
}
.loading-text {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
.row {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
}
.loading-icon {
width: 2.25rem;
height: 2.25rem;
display: block;
:deep(svg) {
left: 1rem;
width: 2.25rem;
height: 2.25rem;
}
}
.show-card-icon {
color: var(--color-brand);
}
.download-enter-active,
.download-leave-active {
transition: opacity 0.3s ease;
}
.download-enter-from,
.download-leave-to {
opacity: 0;
}
.progress-bar {
width: 100%;
}
.info-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
margin: 0;
padding: 0;
}
.info-title {
margin: 0;
}
.profile-button {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
width: 100%;
background-color: var(--color-raised-bg);
box-shadow: none;
.text {
margin-right: auto;
}
}
.profile-card {
position: absolute;
top: 3.5rem;
right: 0.5rem;
z-index: 9;
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-raised);
display: flex;
flex-direction: column;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-button-bg);
padding: var(--gap-md);
&.hidden {
transform: translateY(-100%);
}
}
</style>

View File

@@ -1,222 +0,0 @@
<script setup>
import { ref } from 'vue'
import { Card, DropdownSelect, SearchIcon, XIcon, Button, Avatar } from 'omorphia'
const search = ref('')
const group = ref('Category')
const filters = ref('All profiles')
const sortBy = ref('Name')
defineProps({
showFilters: {
type: Boolean,
default: false,
},
showInstances: {
type: Boolean,
default: false,
},
})
</script>
<template>
<Card class="header" :class="{ highlighted: showFilters }">
<div class="iconified-input">
<SearchIcon />
<input v-model="search" type="text" placeholder="Search" class="search-input" />
<Button @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>
<div class="row">
<section class="instances">
<Card
v-for="project in 20"
:key="project"
class="instance-card-item button-base"
:class="{ highlighted: project === 1 && showInstance }"
>
<Avatar
size="sm"
src="https://launcher-files.modrinth.com/assets/default_profile.png"
alt="Mod card"
class="mod-image"
/>
<div class="project-info">
<p class="title">Example Profile</p>
<p class="description">Forge/Fabric 1.20.1</p>
</div>
</Card>
<slot />
</section>
</div>
</template>
<style lang="scss" scoped>
.row {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
padding: 1rem;
.divider {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 1rem;
margin-bottom: 1rem;
p {
margin: 0;
font-size: 1rem;
white-space: nowrap;
color: var(--color-contrast);
}
hr {
background-color: var(--color-gray);
height: 1px;
width: 100%;
border: none;
}
}
}
.header {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
align-items: inherit;
margin: 1rem 1rem 0 !important;
padding: 1rem;
width: calc(100% - 2rem);
.iconified-input {
flex-grow: 1;
input {
min-width: 100%;
}
}
.sort-dropdown {
width: 10rem;
}
.filter-dropdown {
width: 15rem;
}
.group-dropdown {
width: 10rem;
}
.labeled_button {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
width: 100%;
gap: 1rem;
margin-right: auto;
scroll-behavior: smooth;
}
.instance {
position: relative;
}
.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>

View File

@@ -1,376 +0,0 @@
<script setup>
import {
DownloadIcon,
ChevronRightIcon,
formatNumber,
CalendarIcon,
HeartIcon,
Avatar,
Card,
} from 'omorphia'
import { onMounted, onUnmounted, ref } from 'vue'
const modsRow = ref(null)
const rows = ref(null)
const maxInstancesPerRow = ref(0)
const maxProjectsPerRow = ref(0)
const calculateCardsPerRow = () => {
// Calculate how many cards fit in one row
const containerWidth = rows.value[0].clientWidth
// 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) / 17)
}
onMounted(() => {
calculateCardsPerRow()
window.addEventListener('resize', calculateCardsPerRow)
})
onUnmounted(() => {
window.removeEventListener('resize', calculateCardsPerRow)
})
defineProps({
showInstance: {
type: Boolean,
default: false,
},
})
</script>
<template>
<div class="content">
<div
v-for="(row, index) in ['Jump back in', 'Popular modpacks', 'Popular mods']"
ref="rows"
:key="row"
class="row"
>
<div class="header">
<p>{{ row }}</p>
<ChevronRightIcon />
</div>
<section v-if="index < 1" ref="modsRow" class="instances">
<Card
v-for="project in maxInstancesPerRow"
:key="project"
class="instance-card-item button-base"
:class="{ highlighted: showInstance }"
>
<Avatar
size="sm"
src="https://launcher-files.modrinth.com/assets/default_profile.png"
alt="Mod card"
class="mod-image"
/>
<div class="project-info">
<p class="title">Example Profile</p>
<p class="description">Forge/Fabric 1.20.1</p>
</div>
</Card>
</section>
<section v-else ref="modsRow" class="projects">
<div v-for="project in maxProjectsPerRow" :key="project" class="wrapper">
<Card class="project-card button-base" :class="{ highlighted: showInstance }">
<div
class="banner no-image"
:style="{
'background-image': `url(https://launcher-files.modrinth.com/assets/maze-bg.png)`,
}"
>
<div class="badges">
<div class="badge">
<DownloadIcon />
{{ formatNumber(69420) }}
</div>
<div class="badge">
<HeartIcon />
{{ formatNumber(69) }}
</div>
<div class="badge">
<CalendarIcon />
Today
</div>
</div>
<div
class="badges-wrapper no-image"
:style="{
background:
'linear-gradient(rgba(' +
[27, 217, 106, 0.03].join(',') +
'), 65%, rgba(' +
[27, 217, 106, 0.3].join(',') +
'))',
}"
></div>
</div>
<Avatar
class="icon"
size="sm"
src="https://launcher-files.modrinth.com/assets/default_profile.png"
/>
<div class="title">
<div class="title-text">Example Project</div>
<div class="author">by Modrinth</div>
</div>
<div class="description">
An example project hangin on the Rinth. Very cool project, its probably on Forge and
Fabric. Probably has a 401k and a family.
</div>
</Card>
</div>
</section>
</div>
</div>
</template>
<style lang="scss" scoped>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 1rem;
gap: 1rem;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
}
.row {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
min-width: 100%;
&:nth-child(even) {
background: var(--color-bg);
}
.header {
width: 100%;
margin-bottom: 1rem;
gap: var(--gap-xs);
display: flex;
flex-direction: row;
align-items: center;
p {
margin: 0;
font-size: var(--font-size-lg);
font-weight: bolder;
white-space: nowrap;
color: var(--color-contrast);
}
svg {
height: 1.5rem;
width: 1.5rem;
color: var(--color-contrast);
}
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-gap: 1rem;
width: 100%;
}
.projects {
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
grid-gap: 1rem;
.item {
width: 100%;
max-width: 100%;
}
}
}
.loading-indicator {
width: 2.5rem !important;
height: 2.5rem !important;
svg {
width: 2.5rem !important;
height: 2.5rem !important;
}
}
.instance {
position: relative;
}
.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;
}
}
}
.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: 1;
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);
}
}
</style>

View File

@@ -1,496 +0,0 @@
<script setup>
import { computed, readonly, ref } from 'vue'
import {
Avatar,
Button,
CalendarIcon,
Card,
Categories,
Checkbox,
ClearIcon,
ClientIcon,
DownloadIcon,
DropdownSelect,
EnvironmentIndicator,
formatCategory,
formatCategoryHeader,
formatNumber,
HeartIcon,
NavRow,
Pagination,
Promotion,
SearchFilter,
SearchIcon,
ServerIcon,
StarIcon,
XIcon,
} from 'omorphia'
import Multiselect from 'vue-multiselect'
import { handleError } from '@/store/state'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import SplashScreen from '@/components/ui/SplashScreen.vue'
const loading = ref(false)
const query = ref('')
const facets = ref([])
const orFacets = ref([])
const selectedVersions = ref([])
const onlyOpenSource = ref(false)
const showSnapshots = ref(false)
const selectedEnvironments = ref([])
const sortTypes = readonly([
{ display: 'Relevance', name: 'relevance' },
{ display: 'Download count', name: 'downloads' },
{ display: 'Follow count', name: 'follows' },
{ display: 'Recently published', name: 'newest' },
{ display: 'Recently updated', name: 'updated' },
])
const sortType = ref(sortTypes[0])
const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref('modpack')
const searchWrapper = ref(null)
const sortedCategories = computed(() => {
const values = new Map()
for (const category of categories.value.filter((cat) => cat.project_type === 'mod')) {
if (!values.has(category.header)) {
values.set(category.header, [])
}
values.get(category.header).push(category)
}
return values
})
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories().catch(handleError).then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
])
const pageCount = ref(1)
const selectableProjectTypes = computed(() => {
return [
{ label: 'Shaders', href: `` },
{ label: 'Resource Packs', href: `` },
{ label: 'Data Packs', href: `` },
{ label: 'Mods', href: '' },
{ label: 'Modpacks', href: '' },
]
})
defineProps({
showSearch: {
type: Boolean,
default: false,
},
})
</script>
<template>
<div class="search-container">
<aside class="filter-panel">
<Card class="search-panel-card" :class="{ highlighted: showSearch }">
<Button role="button" disabled> <ClearIcon /> Clear Filters </Button>
<div class="loaders">
<h2>Loaders</h2>
<div
v-for="loader in loaders.filter(
(l) =>
(projectType !== 'mod' && l.supported_project_types?.includes(projectType)) ||
(projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name)),
)"
:key="loader"
>
<SearchFilter
:active-filters="orFacets"
:icon="loader.icon"
:display-name="formatCategory(loader.name)"
:facet-name="`categories:${encodeURIComponent(loader.name)}`"
class="filter-checkbox"
/>
</div>
</div>
<div class="versions">
<h2>Minecraft versions</h2>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
<multiselect
v-model="selectedVersions"
:options="
showSnapshots
? availableGameVersions.map((x) => x.version)
: availableGameVersions
.filter((it) => it.version_type === 'release')
.map((x) => x.version)
"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-search-on-select="false"
:show-labels="false"
placeholder="Choose versions..."
/>
</div>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="facets"
:icon="category.icon"
:display-name="formatCategory(category.name)"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
/>
</div>
</div>
<div v-if="projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Client"
facet-name="client"
class="filter-checkbox"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Server"
facet-name="server"
class="filter-checkbox"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div class="open-source">
<h2>Open source</h2>
<Checkbox v-model="onlyOpenSource" label="Open source only" class="filter-checkbox" />
</div>
</Card>
</aside>
<div ref="searchWrapper" class="search">
<Promotion class="promotion" :external="false" query-param="?r=launcher" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" />
</Card>
<Card class="search-panel-container" :class="{ highlighted: showSearch }">
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="query"
autocomplete="off"
type="text"
:placeholder="`Search ${projectType}s...`"
/>
<Button @click="() => (query = '')">
<XIcon />
</Button>
</div>
<div class="inline-option">
<span>Sort by</span>
<DropdownSelect
v-model="sortType"
name="Sort by"
:options="sortTypes"
:display-name="(option) => option?.display"
/>
</div>
<div class="inline-option">
<span>Show per page</span>
<DropdownSelect
v-model="maxResults"
name="Max results"
:options="[5, 10, 15, 20, 50, 100]"
:default-value="maxResults"
:model-value="maxResults"
class="limit-dropdown"
/>
</div>
</Card>
<Pagination :page="currentPage" :count="pageCount" class="pagination-before" />
<SplashScreen v-if="loading" />
<section v-else class="project-list display-mode--list instance-results" role="list">
<Card v-for="project in 20" :key="project" class="search-card button-base">
<div class="icon">
<Avatar
src="https://launcher-files.modrinth.com/assets/default_profile.png"
size="md"
class="search-icon"
/>
</div>
<div class="content-wrapper">
<div class="title joined-text">
<h2>Example Modpack</h2>
<span>by Modrinth</span>
</div>
<div class="description">
A very cool project that does cool project things that you can your friends can do.
</div>
<div class="tags">
<Categories
:categories="
categories
.filter((cat) => cat.project_type === projectType)
.slice(project / 2, project / 2 + 3)
"
:type="modpack"
>
<EnvironmentIndicator
:type-only="true"
:client-side="true"
:server-side="true"
type="modpack"
:search="true"
/>
</Categories>
</div>
</div>
<div class="stats button-group">
<div v-if="featured" class="badge">
<StarIcon />
Featured
</div>
<div class="badge">
<DownloadIcon />
{{ formatNumber(420) }}
</div>
<div class="badge">
<HeartIcon />
{{ formatNumber(69) }}
</div>
<div class="badge">
<CalendarIcon />
A minute ago
</div>
</div>
</Card>
</section>
<pagination :page="currentPage" :count="pageCount" class="pagination-after" />
</div>
</div>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
.small-instance {
min-height: unset !important;
.instance {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
.title {
font-weight: 600;
color: var(--color-contrast);
}
}
.small-instance_info {
display: flex;
flex-direction: column;
gap: 0.25rem;
justify-content: space-between;
padding: 0.25rem 0;
}
}
.filter-checkbox {
margin-bottom: 0.3rem;
font-size: 1rem;
svg {
display: flex;
align-self: center;
justify-self: center;
}
button.checkbox {
border: none;
}
}
</style>
<style lang="scss" scoped>
.project-type-dropdown {
width: 100% !important;
}
.promotion {
margin-top: 1rem;
}
.project-type-container {
display: flex;
flex-direction: column;
width: 100%;
}
.search-panel-card {
display: flex;
flex-direction: column;
margin-bottom: 0 !important;
min-height: min-content !important;
}
.iconified-input {
input {
max-width: none !important;
flex-basis: auto;
}
}
.search-panel-container {
display: inline-flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
width: 100%;
padding: 1rem !important;
white-space: nowrap;
gap: 1rem;
.inline-option {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
.sort-dropdown {
max-width: 12.25rem;
}
.limit-dropdown {
width: 5rem;
}
}
.iconified-input {
flex-grow: 1;
}
.filter-panel {
button {
display: flex;
align-items: center;
justify-content: space-evenly;
svg {
margin-right: 0.4rem;
}
}
}
}
.search-container {
display: flex;
.filter-panel {
position: fixed;
width: 20rem;
padding: 1rem 0.5rem 1rem 1rem;
display: flex;
flex-direction: column;
height: fit-content;
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
h2 {
color: var(--color-contrast);
margin-top: 1rem;
margin-bottom: 0.5rem;
font-size: 1.16rem;
}
}
.search {
scroll-behavior: smooth;
margin: 0 1rem 0.5rem 20.5rem;
width: calc(100% - 20.5rem);
.loading {
margin: 2rem;
text-align: center;
}
}
}
.search-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;
}
.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;
}
</style>

View File

@@ -1,270 +0,0 @@
<script setup>
import { Card, Slider, DropdownSelect, Toggle } from 'omorphia'
import JavaSelector from '@/components/ui/JavaSelector.vue'
const pageOptions = ['Home', 'Library']
</script>
<template>
<div class="settings-page">
<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="['Dark']"
:disabled="true"
:default-value="'dark'"
class="theme-dropdown disable-children"
/>
</div>
<div class="adjacent-input">
<label for="collapsed-nav">
<span class="label__title">Collapsed navigation mode</span>
<span class="label__description"
>Change the style of the side navigation bar to a compact version.</span
>
</label>
<Toggle id="collapsed-nav" :checked="false" :disabled="true" />
</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" :checked="true" :disabled="true" />
</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" :checked="false" :disabled="true" />
</div>
<div class="opening-page">
<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="Home"
class="opening-page"
:disabled="true"
/>
</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.</span
>
</label>
<Slider id="max-downloads" :min="1" :max="10" :step="1" :disabled="true" />
</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.</span
>
</label>
<Slider id="max-writes" :min="1" :max="50" :step="1" :disabled="true" />
</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">Disable analytics</span>
<span class="label__description">
Modrinth collects anonymized analytics and usage data to improve our user experience and
customize your experience. Opting out will disable this data collection.
</span>
</label>
<Toggle id="opt-out-analytics" :disabled="true" />
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<label for="java-17">
<span class="label__title">Java 17 location</span>
</label>
<JavaSelector id="java-17" :version="17" model-value="" :disabled="true" />
<label for="java-8">
<span class="label__title">Java 8 location</span>
</label>
<JavaSelector id="java-8" :version="8" model-value="" :disabled="true" />
<hr class="card-divider" />
<label for="java-args">
<span class="label__title">Java arguments</span>
</label>
<input
id="java-args"
autocomplete="off"
type="text"
class="installation-input"
placeholder="Enter java arguments..."
:disabled="true"
/>
<label for="env-vars">
<span class="label__title">Environmental variables</span>
</label>
<input
id="env-vars"
autocomplete="off"
type="text"
class="installation-input"
placeholder="Enter environmental variables..."
:disabled="true"
/>
<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" :min="256" :max="10256" :step="1" unit="mb" :disabled="true" />
</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"
autocomplete="off"
type="text"
placeholder="Enter pre-launch command..."
:disabled="true"
/>
</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"
autocomplete="off"
type="text"
placeholder="Enter wrapper command..."
:disabled="true"
/>
</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"
autocomplete="off"
type="text"
placeholder="Enter post-exit command..."
:disabled="true"
/>
</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="width">
<span class="label__title">Width</span>
<span class="label__description"> The width of the game window when launched. </span>
</label>
<input
id="width"
autocomplete="off"
type="number"
placeholder="Enter width..."
:disabled="true"
/>
</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"
autocomplete="off"
type="number"
class="input"
placeholder="Enter height..."
:disabled="true"
/>
</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;
}
.disable-children * {
pointer-events: none;
}
</style>

View File

@@ -1,293 +0,0 @@
<script setup>
import {
Button,
Card,
Checkbox,
Chips,
XIcon,
FolderOpenIcon,
FolderSearchIcon,
UpdatedIcon,
} from 'omorphia'
import { ref } from 'vue'
import {
get_default_launcher_path,
get_importable_instances,
import_instance,
} from '@/helpers/import.js'
import { open } from '@tauri-apps/api/dialog'
import { handleError } from '@/store/state.js'
const props = defineProps({
nextPage: {
type: Function,
required: true,
},
prevPage: {
type: Function,
required: true,
},
})
const profiles = ref(
new Map([
['MultiMC', []],
['GDLauncher', []],
['ATLauncher', []],
['Curseforge', []],
['PrismLauncher', []],
]),
)
const loading = ref(false)
const selectedProfileType = ref('MultiMC')
const profileOptions = ref([
{ name: 'MultiMC', path: '' },
{ name: 'GDLauncher', path: '' },
{ name: 'ATLauncher', path: '' },
{ name: 'Curseforge', path: '' },
{ name: 'PrismLauncher', path: '' },
])
// Attempt to get import profiles on default paths
const promises = profileOptions.value.map(async (option) => {
const path = await get_default_launcher_path(option.name).catch(handleError)
if (!path || path === '') return
// Try catch to allow failure and simply ignore default path attempt
try {
const instances = await get_importable_instances(option.name, path)
if (!instances) return
profileOptions.value.find((profile) => profile.name === option.name).path = path
profiles.value.set(
option.name,
instances.map((name) => ({ name, selected: false })),
)
} catch (error) {
// Allow failure silently
}
})
Promise.all(promises)
const selectLauncherPath = async () => {
selectedProfileType.value.path = await open({ multiple: false, directory: true })
if (selectedProfileType.value.path) {
await reload()
}
}
const reload = async () => {
const instances = await get_importable_instances(
selectedProfileType.value.name,
selectedProfileType.value.path,
).catch(handleError)
profiles.value.set(
selectedProfileType.value.name,
instances.map((name) => ({ name, selected: false })),
)
}
const setPath = () => {
profileOptions.value.find((profile) => profile.name === selectedProfileType.value.name).path =
selectedProfileType.value.path
}
const next = async () => {
loading.value = true
for (const launcher of Array.from(profiles.value.entries()).map(([launcher, profiles]) => ({
launcher,
path: profileOptions.value.find((option) => option.name === launcher).path,
profiles,
}))) {
for (const profile of launcher.profiles.filter((profile) => profile.selected)) {
await import_instance(launcher.launcher, launcher.path, profile.name)
.catch(handleError)
.then(() => console.log(`Successfully Imported ${profile.name} from ${launcher.launcher}`))
}
}
loading.value = false
props.nextPage()
}
</script>
<template>
<Card>
<h2>Importing external profiles</h2>
<Chips
v-model="selectedProfileType"
:items="profileOptions"
:format-label="(profile) => profile?.name"
/>
<div class="path-selection">
<h3>{{ selectedProfileType.name }} path</h3>
<div class="path-input">
<div class="iconified-input">
<FolderOpenIcon />
<input
v-model="selectedProfileType.path"
type="text"
placeholder="Path to launcher"
@change="setPath"
/>
<Button @click="() => (selectedLauncherPath = '')">
<XIcon />
</Button>
</div>
<Button icon-only @click="selectLauncherPath">
<FolderSearchIcon />
</Button>
<Button icon-only @click="reload">
<UpdatedIcon />
</Button>
</div>
</div>
<div class="table">
<div class="table-head table-row">
<div class="toggle-all table-cell">
<Checkbox
class="select-checkbox"
:model-value="profiles.get(selectedProfileType.name)?.every((child) => child.selected)"
@update:model-value="
(newValue) =>
profiles
.get(selectedProfileType.name)
?.forEach((child) => (child.selected = newValue))
"
/>
</div>
<div class="name-cell table-cell">Profile name</div>
</div>
<div
v-if="
profiles.get(selectedProfileType.name) &&
profiles.get(selectedProfileType.name).length > 0
"
class="table-content"
>
<div
v-for="(profile, index) in profiles.get(selectedProfileType.name)"
:key="index"
class="table-row"
>
<div class="checkbox-cell table-cell">
<Checkbox v-model="profile.selected" class="select-checkbox" />
</div>
<div class="name-cell table-cell">
{{ profile.name }}
</div>
</div>
</div>
<div v-else class="table-content empty">No profiles found</div>
</div>
<div class="button-row">
<Button class="transparent" @click="prevPage"> Back </Button>
<Button
:disabled="
loading ||
!Array.from(profiles.values())
.flatMap((e) => e)
.some((e) => e.selected)
"
color="primary"
@click="next"
>
{{
loading
? 'Importing...'
: Array.from(profiles.values())
.flatMap((e) => e)
.some((e) => e.selected)
? `Import ${
Array.from(profiles.values())
.flatMap((e) => e)
.filter((e) => e.selected).length
} profiles`
: 'Select profiles to import'
}}
</Button>
<Button class="transparent" @click="nextPage"> Next </Button>
</div>
</Card>
</template>
<style scoped lang="scss">
.card {
padding: var(--gap-xl);
min-height: unset;
overflow-y: auto;
}
.path-selection {
padding: var(--gap-xl);
background-color: var(--color-bg);
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
margin-bottom: var(--gap-md);
gap: var(--gap-md);
h3 {
margin: 0;
}
.path-input {
display: flex;
align-items: center;
width: 100%;
flex-direction: row;
gap: var(--gap-sm);
.iconified-input {
flex-grow: 1;
:deep(input) {
width: 100%;
flex-basis: auto;
}
}
}
}
.table {
border: 1px solid var(--color-bg);
margin-bottom: var(--gap-md);
}
.table-row {
grid-template-columns: min-content auto;
}
.table-content {
max-height: calc(5 * (18px + 2rem));
height: calc(5 * (18px + 2rem));
overflow-y: auto;
}
.select-checkbox {
button.checkbox {
border: none;
}
}
.button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--gap-md);
.transparent {
padding: var(--gap-sm) 0;
}
}
.empty {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bolder;
color: var(--color-contrast);
}
</style>

View File

@@ -1,18 +1,11 @@
<script setup>
import { Button, LogInIcon, Modal, ClipboardCopyIcon, GlobeIcon, Card } from 'omorphia'
import { authenticate_await_completion, authenticate_begin_flow } from '@/helpers/auth.js'
import { Button, LogInIcon, Card } from 'omorphia'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { handleError } from '@/store/notifications.js'
import { useTheming } from '@/store/theme.js'
import mixpanel from 'mixpanel-browser'
import { get, set } from '@/helpers/settings.js'
import { ref } from 'vue'
import QrcodeVue from 'qrcode.vue'
const themeStore = useTheming()
const loginUrl = ref(null)
const loginModal = ref()
const loginCode = ref(null)
const finalizedLogin = ref(false)
import { handleSevereError } from '@/store/error.js'
const loading = ref(false)
const props = defineProps({
nextPage: {
@@ -26,42 +19,21 @@ const props = defineProps({
})
async function login() {
const loginSuccess = await authenticate_begin_flow().catch(handleError)
loginUrl.value = loginSuccess.verification_uri
loginCode.value = loginSuccess.user_code
loginModal.value.show()
try {
loading.value = true
const loggedIn = await login_flow()
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: loginSuccess.verification_uri,
},
})
if (loggedIn) {
await set_default_user(loggedIn.id).catch(handleError)
}
const loggedIn = await authenticate_await_completion().catch(handleError)
loginModal.value.hide()
const settings = await get().catch(handleError)
settings.default_user = loggedIn.id
await set(settings).catch(handleError)
finalizedLogin.value = true
await mixpanel.track('AccountLogIn')
props.nextPage()
}
const openUrl = async () => {
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: loginUrl.value,
},
})
}
const clipboardWrite = async (a) => {
navigator.clipboard.writeText(a)
await mixpanel.track('AccountLogIn')
loading.value = false
props.nextPage()
} catch (err) {
loading.value = false
handleSevereError(err)
}
}
</script>
@@ -87,45 +59,15 @@ const clipboardWrite = async (a) => {
<div class="action-row">
<Button class="transparent" large @click="prevPage"> Back </Button>
<div class="sign-in-pair">
<Button color="primary" large @click="login">
<LogInIcon v-if="!finalizedLogin" />
{{ finalizedLogin ? 'Next' : 'Sign in' }}
<Button color="primary" large :disabled="loading" @click="login">
<LogInIcon />
{{ loading ? 'Loading...' : 'Sign in' }}
</Button>
</div>
<Button class="transparent" large @click="nextPage()"> Next </Button>
<Button class="transparent" large @click="nextPage()"> Finish</Button>
</div>
</Card>
</div>
<Modal ref="loginModal" header="Signing in" :noblur="!themeStore.advancedRendering">
<div class="modal-body">
<QrcodeVue :value="loginUrl" class="qr-code" margin="3" size="160" />
<div class="modal-text">
<div class="label">Copy this code</div>
<div class="code-text">
<div class="code">
{{ loginCode }}
</div>
<Button
v-tooltip="'Copy code'"
icon-only
large
color="raised"
@click="() => clipboardWrite(loginCode)"
>
<ClipboardCopyIcon />
</Button>
</div>
<div>And enter it on Microsoft's website to sign in.</div>
<div class="iconified-input">
<LogInIcon />
<input type="text" :value="loginUrl" readonly />
<Button v-tooltip="'Open link'" icon-only color="raised" @click="openUrl">
<GlobeIcon />
</Button>
</div>
</div>
</div>
</Modal>
</template>
<style scoped lang="scss">
@@ -188,79 +130,10 @@ const clipboardWrite = async (a) => {
}
}
.qr-code {
background-color: white !important;
border-radius: var(--radius-md);
}
.modal-body {
display: flex;
flex-direction: row;
gap: var(--gap-lg);
align-items: center;
padding: var(--gap-lg);
.modal-text {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
width: 100%;
h2,
p {
margin: 0;
}
}
}
.code-text {
display: flex;
flex-direction: row;
gap: var(--gap-xs);
align-items: center;
.code {
background-color: var(--color-bg);
border-radius: var(--radius-md);
border: solid 1px var(--color-button-bg);
font-family: var(--mono-font);
letter-spacing: var(--gap-md);
color: var(--color-contrast);
font-size: 2rem;
font-weight: bold;
padding: var(--gap-sm) 0 var(--gap-sm) var(--gap-md);
}
.btn {
width: 2.5rem;
height: 2.5rem;
}
}
.sticker {
width: 100%;
max-width: 25rem;
height: auto;
margin-bottom: var(--gap-lg);
}
.sign-in-pair {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
align-items: center;
}
.code {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
.card {
background: var(--color-base);
color: var(--color-contrast);
padding: 0.5rem 1rem;
margin-top: 0.5rem;
}
}
</style>

View File

@@ -1,28 +1,6 @@
<script setup>
import {
Button,
HomeIcon,
SearchIcon,
LibraryIcon,
PlusIcon,
SettingsIcon,
XIcon,
Notifications,
} from 'omorphia'
import { appWindow } from '@tauri-apps/api/window'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import FakeAppBar from '@/components/ui/tutorial/FakeAppBar.vue'
import FakeAccountsCard from '@/components/ui/tutorial/FakeAccountsCard.vue'
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator.js'
import FakeSearch from '@/components/ui/tutorial/FakeSearch.vue'
import FakeGridDisplay from '@/components/ui/tutorial/FakeGridDisplay.vue'
import FakeRowDisplay from '@/components/ui/tutorial/FakeRowDisplay.vue'
import { Button } from 'omorphia'
import { onMounted, ref } from 'vue'
import { window } from '@tauri-apps/api'
import TutorialTip from '@/components/ui/tutorial/TutorialTip.vue'
import FakeSettings from '@/components/ui/tutorial/FakeSettings.vue'
import { get, set } from '@/helpers/settings.js'
import mixpanel from 'mixpanel-browser'
import GalleryImage from '@/components/ui/tutorial/GalleryImage.vue'
@@ -30,11 +8,7 @@ import LoginCard from '@/components/ui/tutorial/LoginCard.vue'
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
import { auto_install_java, get_jre } from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js'
import ImportingCard from '@/components/ui/tutorial/ImportingCard.vue'
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
import PreImportScreen from '@/components/ui/tutorial/PreImportScreen.vue'
const phase = ref(0)
const page = ref(1)
const props = defineProps({
@@ -46,15 +20,6 @@ const props = defineProps({
const flow = ref('')
const nextPhase = () => {
phase.value++
mixpanel.track('TutorialPhase', { page: phase.value })
}
const prevPhase = () => {
phase.value--
}
const nextPage = (newFlow) => {
page.value++
mixpanel.track('OnboardingPage', { page: page.value })
@@ -64,10 +29,6 @@ const nextPage = (newFlow) => {
}
}
const endOnboarding = () => {
nextPhase()
}
const prevPage = () => {
page.value--
}
@@ -105,18 +66,18 @@ onMounted(async () => {
</script>
<template>
<div v-if="phase === 0" class="onboarding">
<div class="onboarding">
<StickyTitleBar />
<GalleryImage
v-if="page === 1"
:gallery="[
{
url: 'https://cdn.discordapp.com/attachments/817413688771608587/1131109353928265809/Screenshot_2023-07-15_at_4.16.18_PM.png',
url: 'https://launcher-files.modrinth.com/onboarding/home.png',
title: 'Discovery',
subtitle: 'See the latest and greatest mods and modpacks to play with from Modrinth',
},
{
url: 'https://cdn.discordapp.com/attachments/817413688771608587/1131109354238640238/Screenshot_2023-07-15_at_4.17.43_PM.png',
url: 'https://launcher-files.modrinth.com/onboarding/discover.png',
title: 'Profile Management',
subtitle:
'Play, manage and search through all the amazing profiles downloaded on your computer at any time, even offline!',
@@ -126,185 +87,7 @@ onMounted(async () => {
>
<Button color="primary" @click="nextPage"> Get started </Button>
</GalleryImage>
<LoginCard v-else-if="page === 2" :next-page="nextPage" :prev-page="prevPage" />
<ModrinthLoginScreen
v-else-if="page === 3"
:modal="false"
:next-page="nextPage"
:prev-page="prevPage"
:flow="flow"
/>
<PreImportScreen
v-else-if="page === 4"
:next-page="endOnboarding"
:prev-page="prevPage"
:import-page="nextPage"
/>
<ImportingCard v-else-if="page === 5" :next-page="endOnboarding" :prev-page="prevPage" />
</div>
<div v-else class="container">
<StickyTitleBar v-if="phase === 9" />
<div v-if="phase < 9" class="nav-container">
<div class="nav-section">
<FakeAccountsCard :show-demo="phase === 3">
<TutorialTip
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Signing in"
description="The Modrinth App uses your Microsoft account to allow you to launch Minecraft. You can sign in with your Microsoft account here, and switch between multiple accounts."
/>
</FakeAccountsCard>
<div class="pages-list">
<div class="btn icon-only" :class="{ active: phase < 4 }">
<HomeIcon />
</div>
<div
class="btn icon-only"
:class="{ active: phase === 4 || phase === 5, highlighted: phase === 4 }"
>
<SearchIcon />
</div>
<div
class="btn icon-only"
:class="{
active: phase === 6 || phase === 7,
highlighted: phase === 6,
}"
>
<LibraryIcon />
</div>
</div>
</div>
<div class="settings pages-list">
<Button class="sleek-primary" icon-only>
<PlusIcon />
</Button>
<Button icon-only :class="{ active: phase === 8, highlighted: phase === 8 }">
<SettingsIcon />
</Button>
</div>
</div>
<div v-if="phase < 9" class="view">
<div data-tauri-drag-region class="appbar">
<section class="navigation-controls">
<Breadcrumbs data-tauri-drag-region />
</section>
<section class="mod-stats">
<FakeAppBar
:show-running="phase === 7"
:show-download="phase === 5"
:exit="finishOnboarding"
>
<template #running>
<TutorialTip
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Playing modpacks"
description="When you launch a modpack, you can manage it directly in the title bar here. You can stop the modpack, view the logs, and see all currently running packs."
/>
</template>
<template #download>
<TutorialTip
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Installing modpacks"
description="When you download a modpack, Modrinth App will automatically install it for you. You can view the progress of the installation here."
/>
</template>
</FakeAppBar>
</section>
<section class="window-controls">
<Button class="titlebar-button" icon-only @click="() => appWindow.minimize()">
<MinimizeIcon />
</Button>
<Button class="titlebar-button" icon-only @click="() => appWindow.toggleMaximize()">
<MaximizeIcon />
</Button>
<Button
class="titlebar-button close"
icon-only
@click="
() => {
saveWindowState(StateFlags.ALL)
window.getCurrent().close()
}
"
>
<XIcon />
</Button>
</section>
</div>
<div class="router-view">
<ModrinthLoadingIndicator
offset-height="var(--appbar-height)"
offset-width="var(--sidebar-width)"
/>
<Notifications ref="notificationsWrapper" />
<FakeRowDisplay v-if="phase < 4 || phase > 8" :show-instance="phase === 2" />
<FakeGridDisplay v-if="phase === 6 || phase === 7" :show-instances="phase === 6" />
<suspense>
<FakeSearch v-if="phase === 4 || phase === 5" :show-search="phase === 4" />
</suspense>
<FakeSettings v-if="phase === 8" />
</div>
</div>
<TutorialTip
v-if="phase === 1"
class="first-tip highlighted"
:progress-function="nextPhase"
:progress="phase"
title="Enter the Modrinth App!"
description="This is the Modrinth App guide. Key parts are marked with a green shadow. Click 'Next' to
proceed. You can leave the tutorial anytime using the Exit button above the plus button on the bottom left."
/>
<div v-if="phase === 1" class="whole-page-shadow" />
<TutorialTip
v-if="phase === 2"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Home page"
description="This is the home page. Here you can see all the latest modpacks, mods, and other content on Modrinth. You can also see a few of your installed modpacks here."
/>
<TutorialTip
v-if="phase === 4"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Searching for content"
description="You can search for content on Modrinth by navigating to the search page. You can search for mods, modpacks, and more, and install them directly from here."
/>
<TutorialTip
v-if="phase === 6"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Modpack library"
description="You can view all your installed modpacks in the library. You can launch any modpack from here, or click the card to view more information about it."
/>
<TutorialTip
v-if="phase === 8"
class="sticky-tip"
:progress-function="nextPhase"
:previous-function="prevPhase"
:progress="phase"
title="Settings"
description="You will be able to view and change the settings for the Modrinth App here. You can change the appearance, set and download new Java versions, and more."
/>
<TutorialTip
v-if="phase === 9"
class="final-tip highlighted"
:progress-function="finishOnboarding"
:progress="phase"
title="Enter the Modrinth App!"
description="That's it! You're ready to use the Modrinth App. If you need help, you can always join our discord server!"
/>
<LoginCard v-else-if="page === 2" :next-page="finishOnboarding" :prev-page="prevPage" />
</div>
</template>

View File

@@ -1,184 +0,0 @@
<script setup>
import { Button, Card, ModrinthIcon } from 'omorphia'
import { ATLauncherIcon, PrismIcon } from '@/assets/external/index.js'
defineProps({
nextPage: {
type: Function,
required: true,
},
prevPage: {
type: Function,
required: true,
},
importPage: {
type: Function,
required: true,
},
})
</script>
<template>
<Card class="import-card">
<div class="base-ellipsis ellipsis-1" />
<div class="base-ellipsis ellipsis-2" />
<div class="base-ellipsis ellipsis-3" />
<div class="base-ellipsis ellipsis-4" />
<div class="logo">
<ModrinthIcon />
</div>
<div class="launcher-stamp top-left">
<ATLauncherIcon />
</div>
<div class="launcher-stamp top-right">
<PrismIcon />
</div>
<div class="launcher-stamp bottom-left">
<img src="@/assets/external/gdlauncher.png" alt="GDLauncher" />
</div>
<div class="launcher-stamp bottom-right">
<img src="@/assets/external/multimc.webp" alt="MultiMC" />
</div>
<div class="info-section">
<h2>Importing</h2>
<div class="markdown-body">
<p>
You can import projects from other launchers by clicking below, or you can skip ahead.
</p>
</div>
<div class="button-row">
<Button class="transparent" @click="prevPage"> Back </Button>
<Button color="primary" @click="importPage"> Import </Button>
<Button class="transparent" @click="nextPage"> Next </Button>
</div>
</div>
</Card>
</template>
<style scoped lang="scss">
.import-card {
width: 40rem;
height: 32rem;
position: relative;
overflow: hidden;
padding: 0;
}
.base-ellipsis {
position: absolute;
left: 50%;
border-radius: 100%;
top: calc(var(--gap-xl) + 5rem);
transform: translate(-50%, -50%);
width: 100%;
background-color: rgba(#1bd96a, 0.1);
}
.ellipsis-1 {
width: 15rem;
height: 15rem;
}
.ellipsis-2 {
width: 30rem;
height: 30rem;
}
.ellipsis-3 {
width: 45rem;
height: 45rem;
}
.logo {
position: absolute;
top: calc(var(--gap-xl) + 5rem);
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
background-color: var(--color-accent-contrast);
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
z-index: 1;
width: 7rem;
height: 7rem;
svg {
width: 100%;
height: 100%;
}
}
.launcher-stamp {
position: absolute;
width: 5rem;
height: 5rem;
background-color: var(--color-accent-contrast);
border-radius: 50%;
z-index: 1;
opacity: 0.65;
padding: var(--gap-lg);
&.top-left {
top: var(--gap-xl);
left: 3rem;
}
&.top-right {
top: var(--gap-xl);
right: 3rem;
}
&.bottom-left {
top: 12rem;
left: 5.5rem;
}
&.bottom-right {
top: 12rem;
right: 5.5rem;
}
svg,
img {
width: 100%;
height: 100%;
}
}
.info-section {
position: absolute;
bottom: var(--gap-xl);
left: 50%;
width: 30rem;
transform: translateX(-50%);
padding: var(--gap-xl);
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
align-items: center;
gap: var(--gap-md);
backdrop-filter: blur(1rem) brightness(0.4);
-webkit-backdrop-filter: blur(1rem) brightness(0.4);
border-radius: var(--radius-lg);
h2 {
margin: 0;
}
}
.button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--gap-md);
width: 100%;
align-content: center;
.transparent {
padding: var(--gap-sm) 0;
}
}
</style>

View File

@@ -1,72 +0,0 @@
<script setup>
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { Button, Card } from 'omorphia'
defineProps({
progress: {
type: Number,
default: 0,
},
title: {
type: String,
default: 'Tutorial',
},
description: {
type: String,
default: 'This is a tutorial',
},
progressFunction: {
type: Function,
default: () => {},
},
previousFunction: {
type: Function,
required: false,
default: null,
},
})
</script>
<template>
<Card class="tutorial-card">
<h3 class="tutorial-title">
{{ title }}
</h3>
<div class="tutorial-body">
{{ description }}
</div>
<div class="tutorial-footer">
<Button v-if="previousFunction" class="transparent" @click="previousFunction"> Back </Button>
{{ progress }}/9
<ProgressBar :progress="(progress / 9) * 100" />
<Button color="primary" :action="progressFunction">
{{ progress === 9 ? 'Finish' : 'Next' }}
</Button>
</div>
</Card>
</template>
<style scoped lang="scss">
.tutorial-card {
display: flex;
flex-direction: column;
gap: var(--gap-md);
border: 1px solid var(--color-button-bg);
width: 22rem;
}
.tutorial-title {
margin: 0;
}
.tutorial-footer {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
.transparent {
border: 1px solid var(--color-button-bg);
}
}
</style>

View File

@@ -18,28 +18,20 @@ import { invoke } from '@tauri-apps/api/tauri'
/// This returns a DeviceLoginSuccess object, with two relevant fields:
/// - verification_uri: the URL to go to to complete the flow
/// - user_code: the code to enter on the verification_uri page
export async function authenticate_begin_flow() {
return await invoke('plugin:auth|auth_authenticate_begin_flow')
export async function login() {
return await invoke('auth_login')
}
/// Authenticate a user with Hydra - part 2
/// This completes the authentication flow quasi-synchronously, returning the sign-in credentials
/// (and also adding the credentials to the state)
/// This returns a Credentials object
export async function authenticate_await_completion() {
return await invoke('plugin:auth|auth_authenticate_await_completion')
}
export async function cancel_flow() {
return await invoke('plugin:auth|auth_cancel_flow')
}
/// Refresh some credentials using Hydra, if needed
/// Retrieves the default user
/// user is UUID
/// update_name is bool
/// Returns a Credentials object
export async function refresh(user, update_name) {
return await invoke('plugin:auth|auth_refresh', { user, update_name })
export async function get_default_user() {
return await invoke('plugin:auth|auth_get_default_user')
}
/// Updates the default user
/// user is UUID
export async function set_default_user(user) {
return await invoke('plugin:auth|auth_set_default_user', { user })
}
/// Remove a user account from the database
@@ -48,13 +40,6 @@ export async function remove_user(user) {
return await invoke('plugin:auth|auth_remove_user', { user })
}
// Add a path as a profile in-memory
// user is UUID
/// Returns a bool
export async function has_user(user) {
return await invoke('plugin:auth|auth_has_user', { user })
}
/// Returns a list of users
/// Returns an Array of Credentials
export async function users() {

View File

@@ -0,0 +1,21 @@
import { defineStore } from 'pinia'
export const useError = defineStore('errorsStore', {
state: () => ({
errorModal: null,
}),
actions: {
setErrorModal(ref) {
this.errorModal = ref
},
showError(error) {
this.errorModal.show(error)
},
},
})
export const handleSevereError = (err) => {
const error = useError()
error.showError(err)
console.error(err)
}