Community requested enhancements (#556)

* Make images lazy and fix #198

* Fix console spam

* Fix bug with bad pagination impl

* Fixes #232

* Finalize more bug fixes

* run lint

* Improve minecraft sign in, improve onboarding

* Linter

* Added back button

* Implement #530

* run linter

* Address changes

* Bump version + run fmt

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
Adrian O.V
2023-08-14 17:02:22 -04:00
committed by GitHub
parent d6ee1ff25a
commit 49bfb0637f
32 changed files with 569 additions and 340 deletions

2
.gitignore vendored
View File

@@ -110,3 +110,5 @@ fabric.properties
# MSVC Windows builds of rustc generate these, which store debugging information # MSVC Windows builds of rustc generate these, which store debugging information
*.pdb *.pdb
theseus.iml

6
Cargo.lock generated
View File

@@ -4609,7 +4609,7 @@ dependencies = [
[[package]] [[package]]
name = "theseus" name = "theseus"
version = "0.5.0" version = "0.5.1"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"async-tungstenite", "async-tungstenite",
@@ -4654,7 +4654,7 @@ dependencies = [
[[package]] [[package]]
name = "theseus_cli" name = "theseus_cli"
version = "0.5.0" version = "0.5.1"
dependencies = [ dependencies = [
"argh", "argh",
"color-eyre", "color-eyre",
@@ -4681,7 +4681,7 @@ dependencies = [
[[package]] [[package]]
name = "theseus_gui" name = "theseus_gui"
version = "0.5.0" version = "0.5.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"cocoa", "cocoa",

11
theseus.iml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/theseus/library" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "theseus" name = "theseus"
version = "0.5.0" version = "0.5.1"
authors = ["Jai A <jaiagr+gpg@pm.me>"] authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018" edition = "2018"

View File

@@ -160,12 +160,11 @@ impl Children {
.signed_duration_since(last_updated_playtime) .signed_duration_since(last_updated_playtime)
.num_seconds(); .num_seconds();
if diff >= 60 { if diff >= 60 {
if let Err(e) = if let Err(e) = profile::edit(&associated_profile, |prof| {
profile::edit(&associated_profile, |mut prof| { prof.metadata.recent_time_played += diff as u64;
prof.metadata.recent_time_played += diff as u64; async { Ok(()) }
async { Ok(()) } })
}) .await
.await
{ {
tracing::warn!( tracing::warn!(
"Failed to update playtime for profile {}: {}", "Failed to update playtime for profile {}: {}",
@@ -181,7 +180,7 @@ impl Children {
let diff = Utc::now() let diff = Utc::now()
.signed_duration_since(last_updated_playtime) .signed_duration_since(last_updated_playtime)
.num_seconds(); .num_seconds();
if let Err(e) = profile::edit(&associated_profile, |mut prof| { if let Err(e) = profile::edit(&associated_profile, |prof| {
prof.metadata.recent_time_played += diff as u64; prof.metadata.recent_time_played += diff as u64;
async { Ok(()) } async { Ok(()) }
}) })

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "theseus_cli" name = "theseus_cli"
version = "0.5.0" version = "0.5.1"
authors = ["Jai A <jaiagr+gpg@pm.me>"] authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018" edition = "2018"

View File

@@ -1,7 +1,7 @@
{ {
"name": "theseus_gui", "name": "theseus_gui",
"private": true, "private": true,
"version": "0.5.0", "version": "0.5.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -18,7 +18,7 @@
"floating-vue": "^2.0.0-beta.20", "floating-vue": "^2.0.0-beta.20",
"mixpanel-browser": "^2.47.0", "mixpanel-browser": "^2.47.0",
"ofetch": "^1.0.1", "ofetch": "^1.0.1",
"omorphia": "^0.4.35", "omorphia": "^0.4.38",
"pinia": "^2.1.3", "pinia": "^2.1.3",
"qrcode.vue": "^3.4.0", "qrcode.vue": "^3.4.0",
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1", "tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",

View File

@@ -21,8 +21,8 @@ dependencies:
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.0.1 version: 1.0.1
omorphia: omorphia:
specifier: ^0.4.35 specifier: ^0.4.38
version: 0.4.35 version: 0.4.38
pinia: pinia:
specifier: ^2.1.3 specifier: ^2.1.3
version: 2.1.3(vue@3.3.4) version: 2.1.3(vue@3.3.4)
@@ -1348,8 +1348,8 @@ packages:
ufo: 1.1.2 ufo: 1.1.2
dev: false dev: false
/omorphia@0.4.35: /omorphia@0.4.38:
resolution: {integrity: sha512-ZxA6sJKWZbiG49l/gTG25cxAvTcIfVSLhuIV2e+LSY0nwkZO4EFvxhzGNz0exR3lVs+OdDCdJyb1U2QYMVbVrA==} resolution: {integrity: sha512-V0vEarmAart6Gf5WuPUZ58TuIiQf7rI5HJpmYU7FVbtdvZ3q08VqyKZflCddbeBSFQ4/N+A+sNr/ELf/jz+Cug==}
dependencies: dependencies:
dayjs: 1.11.7 dayjs: 1.11.7
floating-vue: 2.0.0-beta.20(vue@3.3.4) floating-vue: 2.0.0-beta.20(vue@3.3.4)

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "theseus_gui" name = "theseus_gui"
version = "0.5.0" version = "0.5.1"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
license = "" license = ""

View File

@@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "Modrinth App", "productName": "Modrinth App",
"version": "0.5.0" "version": "0.5.1"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@@ -218,10 +218,11 @@ command_listener((e) => {
<AccountsCard ref="accounts" mode="small" /> <AccountsCard ref="accounts" mode="small" />
</suspense> </suspense>
<div class="pages-list"> <div class="pages-list">
<RouterLink to="/" class="btn icon-only collapsed-button"> <RouterLink v-tooltip="'Home'" to="/" class="btn icon-only collapsed-button">
<HomeIcon /> <HomeIcon />
</RouterLink> </RouterLink>
<RouterLink <RouterLink
v-tooltip="'Browse'"
to="/browse/modpack" to="/browse/modpack"
class="btn icon-only collapsed-button" class="btn icon-only collapsed-button"
:class="{ :class="{
@@ -230,7 +231,7 @@ command_listener((e) => {
> >
<SearchIcon /> <SearchIcon />
</RouterLink> </RouterLink>
<RouterLink to="/library" class="btn icon-only collapsed-button"> <RouterLink v-tooltip="'Library'" to="/library" class="btn icon-only collapsed-button">
<LibraryIcon /> <LibraryIcon />
</RouterLink> </RouterLink>
<Suspense> <Suspense>
@@ -240,6 +241,7 @@ command_listener((e) => {
</div> </div>
<div class="settings pages-list"> <div class="settings pages-list">
<Button <Button
v-tooltip="'Create profile'"
class="sleek-primary collapsed-button" class="sleek-primary collapsed-button"
icon-only icon-only
:disabled="offline" :disabled="offline"
@@ -247,7 +249,7 @@ command_listener((e) => {
> >
<PlusIcon /> <PlusIcon />
</Button> </Button>
<RouterLink to="/settings" class="btn icon-only collapsed-button"> <RouterLink v-tooltip="'Settings'" to="/settings" class="btn icon-only collapsed-button">
<SettingsIcon /> <SettingsIcon />
</RouterLink> </RouterLink>
</div> </div>
@@ -260,7 +262,7 @@ command_listener((e) => {
</section> </section>
<section class="mod-stats"> <section class="mod-stats">
<Suspense> <Suspense>
<RunningAppBar data-tauri-drag-region /> <RunningAppBar />
</Suspense> </Suspense>
</section> </section>
</div> </div>
@@ -290,7 +292,7 @@ command_listener((e) => {
offset-height="var(--appbar-height)" offset-height="var(--appbar-height)"
offset-width="var(--sidebar-width)" offset-width="var(--sidebar-width)"
/> />
<RouterView v-slot="{ Component }" class="main-view"> <RouterView v-slot="{ Component }">
<template v-if="Component"> <template v-if="Component">
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()"> <Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
<component :is="Component"></component> <component :is="Component"></component>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><rect width="8" height="14" x="8" y="6" rx="4"/><path d="m19 7-3 2"/><path d="m5 7 3 2"/><path d="m19 19-3-2"/><path d="m5 19 3-2"/><path d="M20 13h-4"/><path d="M4 13h4"/><path d="m10 4 1 2"/><path d="m14 4-1 2"/></svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@@ -10,3 +10,5 @@ export { default as TextInputIcon } from './text-cursor-input.svg'
export { default as AddProjectImage } from './add-project.svg' export { default as AddProjectImage } from './add-project.svg'
export { default as NewInstanceImage } from './new-instance.svg' export { default as NewInstanceImage } from './new-instance.svg'
export { default as MenuIcon } from './menu.svg' export { default as MenuIcon } from './menu.svg'
export { default as BugIcon } from './bug.svg'
export { default as ChatIcon } from './messages-square.svg'

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-messages-square"><path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2v5Z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@@ -245,6 +245,7 @@ const filteredResults = computed(() => {
<DropdownSelect <DropdownSelect
v-model="sortBy" v-model="sortBy"
class="sort-dropdown" class="sort-dropdown"
name="Sort Dropdown"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']" :options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
placeholder="Select..." placeholder="Select..."
/> />
@@ -254,6 +255,7 @@ const filteredResults = computed(() => {
<DropdownSelect <DropdownSelect
v-model="filters" v-model="filters"
class="filter-dropdown" class="filter-dropdown"
name="Filter Dropdown"
:options="['All profiles', 'Custom instances', 'Downloaded modpacks']" :options="['All profiles', 'Custom instances', 'Downloaded modpacks']"
placeholder="Select..." placeholder="Select..."
/> />
@@ -263,6 +265,7 @@ const filteredResults = computed(() => {
<DropdownSelect <DropdownSelect
v-model="group" v-model="group"
class="group-dropdown" class="group-dropdown"
name="Group Dropdown"
:options="['Category', 'Loader', 'Game version', 'None']" :options="['Category', 'Loader', 'Game version', 'None']"
placeholder="Select..." placeholder="Select..."
/> />

View File

@@ -2,6 +2,7 @@
<div <div
v-if="mode !== 'isolated'" v-if="mode !== 'isolated'"
ref="button" ref="button"
v-tooltip="'Minecraft accounts'"
class="button-base avatar-button" class="button-base avatar-button"
:class="{ expanded: mode === 'expanded' }" :class="{ expanded: mode === 'expanded' }"
@click="showCard = !showCard" @click="showCard = !showCard"
@@ -14,15 +15,6 @@
: 'https://launcher-files.modrinth.com/assets/steve_head.png' : 'https://launcher-files.modrinth.com/assets/steve_head.png'
" "
/> />
<div v-show="mode === 'expanded'" class="avatar-text">
<div class="text no-select">
{{ selectedAccount ? selectedAccount.username : 'Offline' }}
</div>
<p class="accounts-text no-select">
<UsersIcon />
Accounts
</p>
</div>
</div> </div>
<transition name="fade"> <transition name="fade">
<Card <Card
@@ -64,10 +56,50 @@
</Button> </Button>
</Card> </Card>
</transition> </transition>
<Modal ref="loginModal" class="modal" header="Signing in">
<div class="modal-body">
<QrcodeVue :value="loginUrl" class="qr-code" margin="3" size="160" />
<div class="modal-text">
<p>
Sign into Microsoft with your browser. If your browser didn't open, you can copy and open
the link below, or scan the QR code with your device.
</p>
<div class="iconified-input">
<LogInIcon />
<input type="text" :value="loginUrl" readonly />
<Button
v-tooltip="'Copy link'"
icon-only
color="raised"
@click="() => navigator.clipboard.writeText(loginUrl)"
>
<ClipboardCopyIcon />
</Button>
</div>
<div class="button-row">
<Button @click="openUrl">
<GlobeIcon />
Open link
</Button>
<Button class="transparent" @click="loginModal.hide"> Cancel </Button>
</div>
</div>
</div>
</Modal>
</template> </template>
<script setup> <script setup>
import { Avatar, Button, Card, PlusIcon, TrashIcon, UsersIcon, LogInIcon } from 'omorphia' import {
Avatar,
Button,
Card,
PlusIcon,
TrashIcon,
LogInIcon,
Modal,
GlobeIcon,
ClipboardCopyIcon,
} from 'omorphia'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue' import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { import {
users, users,
@@ -76,10 +108,10 @@ import {
authenticate_await_completion, authenticate_await_completion,
} from '@/helpers/auth' } from '@/helpers/auth'
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings'
import { WebviewWindow } from '@tauri-apps/api/window'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { get as getCreds, login_minecraft } from '@/helpers/mr_auth' import { get as getCreds, login_minecraft } from '@/helpers/mr_auth'
import { mixpanel_track } from '@/helpers/mixpanel' import { mixpanel_track } from '@/helpers/mixpanel'
import QrcodeVue from 'qrcode.vue'
defineProps({ defineProps({
mode: { mode: {
@@ -93,6 +125,9 @@ const emit = defineEmits(['change'])
const settings = ref({}) const settings = ref({})
const accounts = ref([]) const accounts = ref([])
const loginUrl = ref('')
const loginModal = ref(null)
async function refreshValues() { async function refreshValues() {
settings.value = await get().catch(handleError) settings.value = await get().catch(handleError)
accounts.value = await users().catch(handleError) accounts.value = await users().catch(handleError)
@@ -118,12 +153,18 @@ async function setAccount(account) {
async function login() { async function login() {
const url = await authenticate_begin_flow().catch(handleError) const url = await authenticate_begin_flow().catch(handleError)
loginUrl.value = url
const window = new WebviewWindow('loginWindow', { await window.__TAURI_INVOKE__('tauri', {
title: 'Modrinth App', __tauriModule: 'Shell',
url: url, message: {
cmd: 'open',
path: url,
},
}) })
loginModal.value.show()
const loggedIn = await authenticate_await_completion().catch(handleError) const loggedIn = await authenticate_await_completion().catch(handleError)
if (loggedIn && loggedIn[0]) { if (loggedIn && loggedIn[0]) {
@@ -139,7 +180,8 @@ async function login() {
} }
} }
} }
await window.close()
loginModal.value.hide()
mixpanel_track('AccountLogIn') mixpanel_track('AccountLogIn')
} }
@@ -329,4 +371,37 @@ onBeforeUnmount(() => {
gap: 0.25rem; gap: 0.25rem;
margin: 0; margin: 0;
} }
.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);
h2,
p {
margin: 0;
}
}
}
.button-row {
display: flex;
flex-direction: row;
}
.modal {
position: absolute;
}
</style> </style>

View File

@@ -1,5 +1,11 @@
<template> <template>
<div class="breadcrumbs"> <div class="breadcrumbs">
<Button class="breadcrumbs__back transparent" icon-only @click="$router.back()">
<ChevronLeftIcon />
</Button>
<Button class="breadcrumbs__forward transparent" icon-only @click="$router.forward()">
<ChevronRightIcon />
</Button>
{{ breadcrumbData.resetToNames(breadcrumbs) }} {{ breadcrumbData.resetToNames(breadcrumbs) }}
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item"> <div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
<router-link <router-link
@@ -25,7 +31,7 @@
</template> </template>
<script setup> <script setup>
import { ChevronRightIcon } from 'omorphia' import { ChevronRightIcon, Button, ChevronLeftIcon } from 'omorphia'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { computed } from 'vue' import { computed } from 'vue'
@@ -60,6 +66,18 @@ const breadcrumbs = computed(() => {
margin: auto 0; margin: auto 0;
} }
} }
.breadcrumbs__back,
.breadcrumbs__forward {
margin: auto 0;
color: var(--color-base);
height: unset;
width: unset;
}
.breadcrumbs__forward {
margin-right: 1rem;
}
} }
.selected { .selected {

View File

@@ -25,6 +25,7 @@
v-model="selectedVersion" v-model="selectedVersion"
:options="versions" :options="versions"
placeholder="Select version" placeholder="Select version"
name="Version select"
:display-name=" :display-name="
(version) => (version) =>
`${version?.name} (${version?.loaders `${version?.name} (${version?.loaders

View File

@@ -1,5 +1,9 @@
<template> <template>
<div class="action-groups"> <div class="action-groups">
<a href="https://discord.gg/modrinth" class="link">
<ChatIcon />
<span> Get support </span>
</a>
<Button <Button
v-if="currentLoadingBars.length > 0" v-if="currentLoadingBars.length > 0"
ref="infoButton" ref="infoButton"
@@ -120,6 +124,7 @@ import { refreshOffline, isOffline } from '@/helpers/utils.js'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { mixpanel_track } from '@/helpers/mixpanel' import { mixpanel_track } from '@/helpers/mixpanel'
import { ChatIcon } from '@/assets/icons'
const router = useRouter() const router = useRouter()
const card = ref(null) const card = ref(null)
@@ -266,7 +271,7 @@ onBeforeUnmount(() => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: var(--gap-sm); gap: var(--gap-md);
} }
.arrow { .arrow {
@@ -452,4 +457,14 @@ onBeforeUnmount(() => {
transform: translateY(-100%); transform: translateY(-100%);
} }
} }
.link {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--gap-sm);
margin: 0;
color: var(--color-text);
text-decoration: none;
}
</style> </style>

View File

@@ -1,5 +1,9 @@
<template> <template>
<div class="action-groups"> <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"> <Button v-if="showDownload" ref="infoButton" icon-only class="icon-button show-card-icon">
<DownloadIcon /> <DownloadIcon />
</Button> </Button>
@@ -36,7 +40,14 @@
</template> </template>
<script setup> <script setup>
import { Button, DownloadIcon, Card, StopCircleIcon, TerminalSquareIcon } from 'omorphia' import {
Button,
DownloadIcon,
Card,
StopCircleIcon,
TerminalSquareIcon,
LogOutIcon,
} from 'omorphia'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
defineProps({ defineProps({
@@ -48,6 +59,10 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
exit: {
type: Function,
required: true,
},
}) })
</script> </script>

View File

@@ -32,6 +32,7 @@ defineProps({
<DropdownSelect <DropdownSelect
v-model="sortBy" v-model="sortBy"
class="sort-dropdown" class="sort-dropdown"
name="Sort Dropdown"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']" :options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
placeholder="Select..." placeholder="Select..."
/> />
@@ -41,6 +42,7 @@ defineProps({
<DropdownSelect <DropdownSelect
v-model="filters" v-model="filters"
class="filter-dropdown" class="filter-dropdown"
name="Filter Dropdown"
:options="['All profiles', 'Custom instances', 'Downloaded modpacks']" :options="['All profiles', 'Custom instances', 'Downloaded modpacks']"
placeholder="Select..." placeholder="Select..."
/> />
@@ -50,6 +52,7 @@ defineProps({
<DropdownSelect <DropdownSelect
v-model="group" v-model="group"
class="group-dropdown" class="group-dropdown"
name="Group dropdown"
:options="['Category', 'Loader', 'Game version', 'None']" :options="['Category', 'Loader', 'Game version', 'None']"
placeholder="Select..." placeholder="Select..."
/> />

View File

@@ -63,7 +63,7 @@ defineProps({
> >
<Avatar <Avatar
size="sm" size="sm"
src="https://launcher-files.modrinth.com/assets/maze-bg.png" src="https://launcher-files.modrinth.com/assets/default_profile.png"
alt="Mod card" alt="Mod card"
class="mod-image" class="mod-image"
/> />

View File

@@ -176,7 +176,7 @@ defineProps({
</Card> </Card>
</aside> </aside>
<div ref="searchWrapper" class="search"> <div ref="searchWrapper" class="search">
<Promotion class="promotion" /> <Promotion class="promotion" query-param="?r=launcher" />
<Card class="project-type-container"> <Card class="project-type-container">
<NavRow :links="selectableProjectTypes" /> <NavRow :links="selectableProjectTypes" />
</Card> </Card>

View File

@@ -8,7 +8,6 @@ import {
SettingsIcon, SettingsIcon,
XIcon, XIcon,
Notifications, Notifications,
LogOutIcon,
} from 'omorphia' } from 'omorphia'
import { appWindow } from '@tauri-apps/api/window' import { appWindow } from '@tauri-apps/api/window'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api' import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
@@ -160,7 +159,10 @@ onMounted(async () => {
<div class="btn icon-only" :class="{ active: phase < 4 }"> <div class="btn icon-only" :class="{ active: phase < 4 }">
<HomeIcon /> <HomeIcon />
</div> </div>
<div class="btn icon-only" :class="{ active: phase === 4 || phase === 5 }"> <div
class="btn icon-only"
:class="{ active: phase === 4 || phase === 5, highlighted: phase === 4 }"
>
<SearchIcon /> <SearchIcon />
</div> </div>
<div <div
@@ -175,9 +177,6 @@ onMounted(async () => {
</div> </div>
</div> </div>
<div class="settings pages-list"> <div class="settings pages-list">
<Button class="active" icon-only @click="finishOnboarding">
<LogOutIcon />
</Button>
<Button class="sleek-primary" icon-only> <Button class="sleek-primary" icon-only>
<PlusIcon /> <PlusIcon />
</Button> </Button>
@@ -192,7 +191,11 @@ onMounted(async () => {
<Breadcrumbs data-tauri-drag-region /> <Breadcrumbs data-tauri-drag-region />
</section> </section>
<section class="mod-stats"> <section class="mod-stats">
<FakeAppBar :show-running="phase === 7" :show-download="phase === 5"> <FakeAppBar
:show-running="phase === 7"
:show-download="phase === 5"
:exit="finishOnboarding"
>
<template #running> <template #running>
<TutorialTip <TutorialTip
:progress-function="nextPhase" :progress-function="nextPhase"
@@ -430,12 +433,6 @@ onMounted(async () => {
background-color: var(--color-brand-highlight); background-color: var(--color-brand-highlight);
transition: all ease-in-out 0.1s; transition: all ease-in-out 0.1s;
} }
&.sleek-exit {
background-color: var(--color-red);
color: var(--color-accent-contrast);
transition: all ease-in-out 0.1s;
}
} }
} }

View File

@@ -29,7 +29,7 @@ import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue' import SplashScreen from '@/components/ui/SplashScreen.vue'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue' import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { check_installed, get as getInstance } from '@/helpers/profile.js' import { check_installed, get, get as getInstance } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { isOffline } from '@/helpers/utils' import { isOffline } from '@/helpers/utils'
import { offline_listener } from '@/helpers/events' import { offline_listener } from '@/helpers/events'
@@ -56,6 +56,7 @@ const orFacets = ref([])
const selectedVersions = ref([]) const selectedVersions = ref([])
const onlyOpenSource = ref(false) const onlyOpenSource = ref(false)
const showSnapshots = ref(false) const showSnapshots = ref(false)
const hideAlreadyInstalled = ref(false)
const selectedEnvironments = ref([]) const selectedEnvironments = ref([])
const sortTypes = readonly([ const sortTypes = readonly([
{ display: 'Relevance', name: 'relevance' }, { display: 'Relevance', name: 'relevance' },
@@ -143,6 +144,9 @@ if (route.query.m) {
if (route.query.o) { if (route.query.o) {
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1 currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1
} }
if (route.query.ai) {
hideAlreadyInstalled.value = route.query.ai === 'true'
}
async function refreshSearch() { async function refreshSearch() {
const base = 'https://api.modrinth.com/v2/' const base = 'https://api.modrinth.com/v2/'
@@ -222,6 +226,16 @@ async function refreshSearch() {
]) ])
} }
if (hideAlreadyInstalled.value) {
const installedMods = await get(instanceContext.value.path, false).then((x) =>
Object.values(x.projects)
.filter((x) => x.metadata.project)
.map((x) => x.metadata.project.id)
)
installedMods.map((x) => [`project_id != ${x}`]).forEach((x) => formattedFacets.push(x))
console.log(`facets=${JSON.stringify(formattedFacets)}`)
}
params.push(`facets=${JSON.stringify(formattedFacets)}`) params.push(`facets=${JSON.stringify(formattedFacets)}`)
} }
const offset = (currentPage.value - 1) * maxResults.value const offset = (currentPage.value - 1) * maxResults.value
@@ -339,6 +353,10 @@ function getSearchUrl(offset, useObj) {
queryItems.push('il=true') queryItems.push('il=true')
obj.il = true obj.il = true
} }
if (hideAlreadyInstalled.value) {
queryItems.push('ai=true')
obj.ai = true
}
let url = `${route.path}` let url = `${route.path}`
@@ -555,6 +573,13 @@ onUnmounted(() => unlistenOffline())
@update:model-value="onSearchChangeToTop(1)" @update:model-value="onSearchChangeToTop(1)"
@click.prevent.stop @click.prevent.stop
/> />
<Checkbox
v-model="hideAlreadyInstalled"
label="Hide already installed"
class="filter-checkbox"
@update:model-value="onSearchChangeToTop(1)"
@click.prevent.stop
/>
</Card> </Card>
<Card class="search-panel-card"> <Card class="search-panel-card">
<Button <Button
@@ -662,7 +687,7 @@ onUnmounted(() => unlistenOffline())
</Card> </Card>
</aside> </aside>
<div class="search"> <div class="search">
<Promotion class="promotion" :external="false" /> <Promotion class="promotion" :external="false" query-param="?r=launcher" />
<Card class="project-type-container"> <Card class="project-type-container">
<NavRow :links="selectableProjectTypes" /> <NavRow :links="selectableProjectTypes" />
</Card> </Card>
@@ -711,7 +736,7 @@ onUnmounted(() => unlistenOffline())
@switch-page="onSearchChange" @switch-page="onSearchChange"
/> />
<SplashScreen v-if="loading" /> <SplashScreen v-if="loading" />
<section v-else-if="offline && results.total_hits == 0" class="offline"> <section v-else-if="offline && results.total_hits === 0" class="offline">
You are currently offline. Connect to the internet to browse Modrinth! You are currently offline. Connect to the internet to browse Modrinth!
</section> </section>
<section v-else class="project-list display-mode--list instance-results" role="list"> <section v-else class="project-list display-mode--list instance-results" role="list">

View File

@@ -35,12 +35,7 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<GridDisplay <GridDisplay v-if="instances.length > 0" label="Instances" :instances="instances" />
v-if="instances.length > 0"
label="Instances"
:instances="instances"
class="display"
/>
<div v-else class="no-instance"> <div v-else class="no-instance">
<div class="icon"> <div class="icon">
<NewInstanceImage /> <NewInstanceImage />
@@ -55,11 +50,6 @@ onUnmounted(() => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.display {
background-color: rgb(30, 31, 34);
min-height: 100%;
}
.no-instance { .no-instance {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -308,7 +308,8 @@ async function refreshDir() {
<span class="label__title">Disable analytics</span> <span class="label__title">Disable analytics</span>
<span class="label__description"> <span class="label__description">
Modrinth collects anonymized analytics and usage data to improve our user experience and Modrinth collects anonymized analytics and usage data to improve our user experience and
customize your experience. Opting out will disable this data collection. customize your experience. By enabling this option, you opt out and your data will no
longer be collected.
</span> </span>
</label> </label>
<Toggle <Toggle

View File

@@ -72,17 +72,10 @@
Options Options
</RouterLink> </RouterLink>
</div> </div>
<hr class="card-divider" />
<div class="pages-list">
<Button class="transparent" @click="exportModal.show()">
<PackageIcon />
Export modpack
</Button>
</div>
</Card> </Card>
</div> </div>
<div class="content"> <div class="content">
<Promotion /> <Promotion query-param="?r=launcher" />
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<template v-if="Component"> <template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()"> <Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
@@ -118,7 +111,6 @@
> >
<template #filter_update><UpdatedIcon />Select Updatable</template> <template #filter_update><UpdatedIcon />Select Updatable</template>
</ContextMenu> </ContextMenu>
<ExportModal ref="exportModal" :instance="instance" />
</template> </template>
<script setup> <script setup>
import { import {
@@ -156,15 +148,12 @@ import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { isOffline, showProfileInFolder } from '@/helpers/utils.js' import { isOffline, showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import { mixpanel_track } from '@/helpers/mixpanel' import { mixpanel_track } from '@/helpers/mixpanel'
import { PackageIcon } from '@/assets/icons/index.js'
import ExportModal from '@/components/ui/ExportModal.vue'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
const exportModal = ref(null)
const instance = ref(await get(route.params.id).catch(handleError)) const instance = ref(await get(route.params.id).catch(handleError))

View File

@@ -1,26 +1,13 @@
<template> <template>
<Card <Card v-if="projects.length > 0" class="mod-card">
v-if="projects.length > 0" <div class="dropdown-input">
class="mod-card" <DropdownSelect
:class="{ static: instance.metadata.linked_data }"
>
<div class="second-row">
<Chips
v-if="Object.keys(selectableProjectTypes).length > 1"
v-model="selectedProjectType" v-model="selectedProjectType"
:items="Object.keys(selectableProjectTypes)" :options="Object.keys(selectableProjectTypes)"
default-value="All"
name="project-type-dropdown"
color="primary"
/> />
<Button
v-if="canUpdatePack"
:disabled="updatingModpack"
color="secondary"
@click="updateModpack"
>
<UpdatedIcon />
{{ updatingModpack ? 'Updating' : 'Update modpack' }}
</Button>
</div>
<div class="card-row">
<div class="iconified-input"> <div class="iconified-input">
<SearchIcon /> <SearchIcon />
<input <input
@@ -37,228 +24,253 @@
<XIcon /> <XIcon />
</Button> </Button>
</div> </div>
<span class="manage">
<DropdownButton
:options="['search', 'from_file']"
default-value="search"
name="add-content-dropdown"
color="primary"
@option-click="handleContentOptionClick"
>
<template #search>
<SearchIcon />
<span class="no-wrap"> Add content </span>
</template>
<template #from_file>
<FolderOpenIcon />
<span class="no-wrap"> Add from file </span>
</template>
</DropdownButton>
</span>
</div> </div>
<div> <Button
<div class="table"> v-if="isPackLinked"
<div class="table-row table-head" :class="{ 'show-options': selected.length > 0 }"> v-tooltip="'Modpack is up to date'"
<div v-if="!instance.metadata.linked_data" class="table-cell table-text"> :disabled="updatingModpack || !canUpdatePack"
<Checkbox v-model="selectAll" class="select-checkbox" /> color="secondary"
</div> @click="updateModpack"
<div v-if="selected.length === 0" class="table-cell table-text name-cell actions-cell"> >
<Button class="transparent" @click="sortProjects('Name')"> <UpdatedIcon />
Name {{ updatingModpack ? 'Updating' : 'Update modpack' }}
<DropdownIcon v-if="sortColumn === 'Name'" :class="{ down: ascending }" /> </Button>
</Button> <Button v-else @click="exportModal.show()">
</div> <PackageIcon />
<div v-if="selected.length === 0" class="table-cell table-text version"> Export modpack
<Button class="transparent" @click="sortProjects('Version')"> </Button>
Version <DropdownButton
<DropdownIcon v-if="sortColumn === 'Version'" :class="{ down: ascending }" /> v-if="!isPackLinked"
</Button> :options="['search', 'from_file']"
</div> default-value="search"
<div v-if="selected.length === 0" class="table-cell table-text actions-cell"> name="add-content-dropdown"
<Button color="primary"
v-if="!instance.metadata.linked_data" @option-click="handleContentOptionClick"
class="transparent" >
@click="sortProjects('Enabled')" <template #search>
> <SearchIcon />
Actions <span class="no-wrap"> Add content </span>
<DropdownIcon v-if="sortColumn === 'Enabled'" :class="{ down: ascending }" /> </template>
</Button> <template #from_file>
</div> <FolderOpenIcon />
<div v-else-if="!instance.metadata.linked_data" class="options table-cell name-cell"> <span class="no-wrap"> Add from file </span>
<Button </template>
class="transparent share" </DropdownButton>
@click="() => (showingOptions = !showingOptions)" </Card>
@mouseover="selectedOption = 'Share'" <Pagination
> v-if="projects.length > 0"
<MenuIcon :class="{ open: showingOptions }" /> :page="currentPage"
</Button> :count="Math.ceil(search.length / 20)"
<Button class="pagination-before"
class="transparent share" :link-function="(page) => `?page=${page}`"
@click="shareNames()" @switch-page="switchPage"
@mouseover="selectedOption = 'Share'" />
> <Card
<ShareIcon /> v-if="projects.length > 0"
Share class="list-card"
</Button> :class="{ static: instance.metadata.linked_data }"
<Button >
class="transparent trash" <div class="table">
@click="deleteWarning.show()" <div class="table-row table-head" :class="{ 'show-options': selected.length > 0 }">
@mouseover="selectedOption = 'Delete'" <div v-if="!instance.metadata.linked_data" class="table-cell table-text">
> <Checkbox v-model="selectAll" class="select-checkbox" />
<TrashIcon />
Delete
</Button>
<Button
class="transparent update"
:disabled="offline"
@click="updateAll()"
@mouseover="selectedOption = 'Update'"
>
<UpdatedIcon />
Update
</Button>
<Button
class="transparent"
@click="toggleSelected()"
@mouseover="selectedOption = 'Toggle'"
>
<ToggleIcon />
Toggle
</Button>
</div>
</div> </div>
<div <div v-if="selected.length === 0" class="table-cell table-text name-cell actions-cell">
v-if="showingOptions && selected.length > 0 && !instance.metadata.linked_data" <Button class="transparent" @click="sortProjects('Name')">
class="more-box" Name
> <DropdownIcon v-if="sortColumn === 'Name'" :class="{ down: ascending }" />
<section v-if="selectedOption === 'Share'" class="options"> </Button>
<Button class="transparent" @click="shareNames()">
<TextInputIcon />
Share names
</Button>
<Button class="transparent" @click="shareUrls()">
<GlobeIcon />
Share URLs
</Button>
<Button class="transparent" @click="shareFileNames()">
<FileIcon />
Share file names
</Button>
<Button class="transparent" @click="shareMarkdown()">
<CodeIcon />
Share as markdown
</Button>
</section>
<section v-if="selectedOption === 'Delete'" class="options">
<Button class="transparent" @click="deleteWarning.show()">
<TrashIcon />
Delete selected
</Button>
<Button class="transparent" @click="deleteDisabledWarning.show()">
<ToggleIcon />
Delete disabled
</Button>
</section>
<section v-if="selectedOption === 'Update'" class="options">
<Button class="transparent" :disabled="offline" @click="updateAll()">
<UpdatedIcon />
Update all
</Button>
<Button class="transparent" @click="selectUpdatable()">
<CheckIcon />
Select updatable
</Button>
</section>
<section v-if="selectedOption === 'Toggle'" class="options">
<Button class="transparent" @click="enableAll()">
<CheckIcon />
Toggle on
</Button>
<Button class="transparent" @click="disableAll()">
<XIcon />
Toggle off
</Button>
<Button class="transparent" @click="hideShowAll()">
<EyeIcon v-if="hideNonSelected" />
<EyeOffIcon v-else />
{{ hideNonSelected ? 'Show' : 'Hide' }} untoggled
</Button>
</section>
</div> </div>
<div <div v-if="selected.length === 0" class="table-cell table-text version">
v-for="mod in search" <Button class="transparent" @click="sortProjects('Version')">
:key="mod.file_name" Version
class="table-row" <DropdownIcon v-if="sortColumn === 'Version'" :class="{ down: ascending }" />
@contextmenu.prevent.stop="(c) => handleRightClick(c, mod)" </Button>
> </div>
<div v-if="!instance.metadata.linked_data" class="table-cell table-text checkbox"> <div v-if="selected.length === 0" class="table-cell table-text actions-cell">
<Checkbox <Button
:model-value="selectionMap.get(mod.path)" v-if="!instance.metadata.linked_data"
class="select-checkbox" class="transparent"
@update:model-value="(newValue) => selectionMap.set(mod.path, newValue)" @click="sortProjects('Enabled')"
/> >
</div> Actions
<div class="table-cell table-text name-cell"> <DropdownIcon v-if="sortColumn === 'Enabled'" :class="{ down: ascending }" />
<router-link </Button>
v-if="mod.slug" </div>
:to="{ path: `/project/${mod.slug}/`, query: { i: props.instance.path } }" <div v-else-if="!instance.metadata.linked_data" class="options table-cell name-cell">
:disabled="offline" <Button
class="mod-content" class="transparent share"
> @click="() => (showingOptions = !showingOptions)"
<Avatar :src="mod.icon" /> @mouseover="selectedOption = 'Share'"
<div v-tooltip="`${mod.name} by ${mod.author}`" class="mod-text"> >
<div class="title">{{ mod.name }}</div> <MenuIcon :class="{ open: showingOptions }" />
<span class="no-wrap">by {{ mod.author }}</span> </Button>
</div> <Button
</router-link> class="transparent share"
<div v-else class="mod-content"> @click="shareNames()"
<Avatar :src="mod.icon" /> @mouseover="selectedOption = 'Share'"
<span v-tooltip="`${mod.name}`" class="title">{{ mod.name }}</span> >
<ShareIcon />
Share
</Button>
<Button
class="transparent trash"
@click="deleteWarning.show()"
@mouseover="selectedOption = 'Delete'"
>
<TrashIcon />
Delete
</Button>
<Button
class="transparent update"
:disabled="offline"
@click="updateAll()"
@mouseover="selectedOption = 'Update'"
>
<UpdatedIcon />
Update
</Button>
<Button
class="transparent"
@click="toggleSelected()"
@mouseover="selectedOption = 'Toggle'"
>
<ToggleIcon />
Toggle
</Button>
</div>
</div>
<div
v-if="showingOptions && selected.length > 0 && !instance.metadata.linked_data"
class="more-box"
>
<section v-if="selectedOption === 'Share'" class="options">
<Button class="transparent" @click="shareNames()">
<TextInputIcon />
Share names
</Button>
<Button class="transparent" @click="shareUrls()">
<GlobeIcon />
Share URLs
</Button>
<Button class="transparent" @click="shareFileNames()">
<FileIcon />
Share file names
</Button>
<Button class="transparent" @click="shareMarkdown()">
<CodeIcon />
Share as markdown
</Button>
</section>
<section v-if="selectedOption === 'Delete'" class="options">
<Button class="transparent" @click="deleteWarning.show()">
<TrashIcon />
Delete selected
</Button>
<Button class="transparent" @click="deleteDisabledWarning.show()">
<ToggleIcon />
Delete disabled
</Button>
</section>
<section v-if="selectedOption === 'Update'" class="options">
<Button class="transparent" :disabled="offline" @click="updateAll()">
<UpdatedIcon />
Update all
</Button>
<Button class="transparent" @click="selectUpdatable()">
<CheckIcon />
Select updatable
</Button>
</section>
<section v-if="selectedOption === 'Toggle'" class="options">
<Button class="transparent" @click="enableAll()">
<CheckIcon />
Toggle on
</Button>
<Button class="transparent" @click="disableAll()">
<XIcon />
Toggle off
</Button>
<Button class="transparent" @click="hideShowAll()">
<EyeIcon v-if="hideNonSelected" />
<EyeOffIcon v-else />
{{ hideNonSelected ? 'Show' : 'Hide' }} untoggled
</Button>
</section>
</div>
<div
v-for="mod in search.slice((currentPage - 1) * 20, currentPage * 20)"
:key="mod.file_name"
class="table-row"
@contextmenu.prevent.stop="(c) => handleRightClick(c, mod)"
>
<div v-if="!instance.metadata.linked_data" class="table-cell table-text checkbox">
<Checkbox
:model-value="selectionMap.get(mod.path)"
class="select-checkbox"
@update:model-value="(newValue) => selectionMap.set(mod.path, newValue)"
/>
</div>
<div class="table-cell table-text name-cell">
<router-link
v-if="mod.slug"
:to="{ path: `/project/${mod.slug}/`, query: { i: props.instance.path } }"
:disabled="offline"
class="mod-content"
>
<Avatar :src="mod.icon" />
<div v-tooltip="`${mod.name} by ${mod.author}`" class="mod-text">
<div class="title">{{ mod.name }}</div>
<span class="no-wrap">by {{ mod.author }}</span>
</div> </div>
</router-link>
<div v-else class="mod-content">
<Avatar :src="mod.icon" />
<span v-tooltip="`${mod.name}`" class="title">{{ mod.name }}</span>
</div> </div>
<div class="table-cell table-text version"> </div>
<span v-tooltip="`${mod.version}`">{{ mod.version }}</span> <div class="table-cell table-text version">
</div> <span v-tooltip="`${mod.version}`">{{ mod.version }}</span>
<div class="table-cell table-text manage"> </div>
<Button <div class="table-cell table-text manage">
v-if="!instance.metadata.linked_data" <Button
v-tooltip="'Remove project'" v-if="!instance.metadata.linked_data"
icon-only v-tooltip="'Remove project'"
@click="removeMod(mod)" icon-only
> @click="removeMod(mod)"
<TrashIcon /> >
</Button> <TrashIcon />
<AnimatedLogo </Button>
v-if="mod.updating && !instance.metadata.linked_data" <AnimatedLogo
class="btn icon-only updating-indicator" v-if="mod.updating && !instance.metadata.linked_data"
></AnimatedLogo> class="btn icon-only updating-indicator"
<Button ></AnimatedLogo>
v-else-if="!instance.metadata.linked_data" <Button
v-tooltip="'Update project'" v-else-if="!instance.metadata.linked_data"
:disabled="!mod.outdated || offline" v-tooltip="'Update project'"
icon-only :disabled="!mod.outdated || offline"
@click="updateProject(mod)" icon-only
> @click="updateProject(mod)"
<UpdatedIcon v-if="mod.outdated" /> >
<CheckIcon v-else /> <UpdatedIcon v-if="mod.outdated" />
</Button> <CheckIcon v-else />
<input </Button>
v-if="!instance.metadata.linked_data" <input
id="switch-1" v-if="!instance.metadata.linked_data"
autocomplete="off" id="switch-1"
type="checkbox" autocomplete="off"
class="switch stylized-toggle" type="checkbox"
:checked="!mod.disabled" class="switch stylized-toggle"
@change="toggleDisableMod(mod)" :checked="!mod.disabled"
/> @change="toggleDisableMod(mod)"
<Button />
v-tooltip="`Show ${mod.file_name}`" <Button
icon-only v-tooltip="`Show ${mod.file_name}`"
@click="showProfileInFolder(mod.path)" icon-only
> @click="showProfileInFolder(mod.path)"
<FolderOpenIcon /> >
</Button> <FolderOpenIcon />
</div> </Button>
</div> </div>
</div> </div>
</div> </div>
@@ -335,6 +347,7 @@
share-title="Sharing modpack content" share-title="Sharing modpack content"
share-text="Check out the projects I'm using in my modpack!" share-text="Check out the projects I'm using in my modpack!"
/> />
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
</template> </template>
<script setup> <script setup>
import { import {
@@ -346,7 +359,6 @@ import {
SearchIcon, SearchIcon,
UpdatedIcon, UpdatedIcon,
AnimatedLogo, AnimatedLogo,
Chips,
FolderOpenIcon, FolderOpenIcon,
Checkbox, Checkbox,
formatProjectType, formatProjectType,
@@ -361,6 +373,8 @@ import {
EyeOffIcon, EyeOffIcon,
ShareModal, ShareModal,
CodeIcon, CodeIcon,
Pagination,
DropdownSelect,
} from 'omorphia' } from 'omorphia'
import { computed, onUnmounted, ref, watch } from 'vue' import { computed, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -379,7 +393,8 @@ import { open } from '@tauri-apps/api/dialog'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage } from '@/assets/icons' import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
import ExportModal from '@/components/ui/ExportModal.vue'
const router = useRouter() const router = useRouter()
@@ -407,12 +422,13 @@ const props = defineProps({
const projects = ref([]) const projects = ref([])
const selectionMap = ref(new Map()) const selectionMap = ref(new Map())
const showingOptions = ref(false) const showingOptions = ref(false)
const canUpdatePack = computed(() => { const isPackLinked = computed(() => {
return ( return props.instance.metadata.linked_data
props.instance.metadata.linked_data &&
props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
)
}) })
const canUpdatePack = computed(() => {
return props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
})
const exportModal = ref(null)
console.log(props.instance) console.log(props.instance)
const initProjects = (initInstance) => { const initProjects = (initInstance) => {
@@ -501,6 +517,7 @@ const selectedOption = ref('Share')
const shareModal = ref(null) const shareModal = ref(null)
const ascending = ref(true) const ascending = ref(true)
const sortColumn = ref('Name') const sortColumn = ref('Name')
const currentPage = ref(1)
const selected = computed(() => const selected = computed(() =>
Array.from(selectionMap.value) Array.from(selectionMap.value)
@@ -833,6 +850,10 @@ const unlisten = await listen('tauri://file-drop', async (event) => {
initProjects(await get(props.instance.path).catch(handleError)) initProjects(await get(props.instance.path).catch(handleError))
}) })
const switchPage = (page) => {
currentPage.value = page
}
onUnmounted(() => { onUnmounted(() => {
unlisten() unlisten()
}) })
@@ -903,10 +924,56 @@ onUnmounted(() => {
.mod-card { .mod-card {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
flex-wrap: wrap;
gap: var(--gap-sm); gap: var(--gap-sm);
justify-content: center; justify-content: flex-start;
overflow: hidden; margin-bottom: 0.5rem;
white-space: nowrap;
align-items: center;
:deep(.dropdown-row) {
.btn {
height: 2.5rem !important;
}
}
.btn {
height: 2.5rem;
}
.dropdown-input {
flex-grow: 1;
.iconified-input {
width: 100%;
input {
flex-basis: unset;
}
}
:deep(.animated-dropdown) {
.render-down {
border-radius: var(--radius-md) 0 0 var(--radius-md) !important;
}
.options-wrapper {
margin-top: 0.25rem;
width: unset;
border-radius: var(--radius-md);
}
.options {
border-radius: var(--radius-md);
border: 1px solid var(--color);
}
}
}
}
.list-card {
margin-top: 0.5rem;
} }
.text-combo { .text-combo {
@@ -1078,4 +1145,10 @@ onUnmounted(() => {
margin: 0; margin: 0;
} }
} }
.dropdown-input {
.selected {
height: 2.5rem;
}
}
</style> </style>

View File

@@ -21,7 +21,12 @@
<div class="input-row"> <div class="input-row">
<p class="input-label">Game Version</p> <p class="input-label">Game Version</p>
<div class="versions"> <div class="versions">
<DropdownSelect v-model="gameVersion" :options="selectableGameVersions" render-up /> <DropdownSelect
v-model="gameVersion"
:options="selectableGameVersions"
name="Game Version Dropdown"
render-up
/>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" /> <Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
</div> </div>
</div> </div>
@@ -31,6 +36,7 @@
:model-value="selectableLoaderVersions[loaderVersionIndex]" :model-value="selectableLoaderVersions[loaderVersionIndex]"
:options="selectableLoaderVersions" :options="selectableLoaderVersions"
:display-name="(option) => option?.id" :display-name="(option) => option?.id"
name="Version selector"
render-up render-up
@change="(value) => (loaderVersionIndex = value.index)" @change="(value) => (loaderVersionIndex = value.index)"
/> />
@@ -426,11 +432,9 @@ const groups = ref(props.instance.metadata.groups)
const instancesList = Object.values(await list(true)) const instancesList = Object.values(await list(true))
const availableGroups = ref([ const availableGroups = ref([
...new Set( ...instancesList.reduce((acc, obj) => {
instancesList.reduce((acc, obj) => { return acc.concat(obj.metadata.groups)
return acc.concat(obj.metadata.groups) }, []),
}, [])
),
]) ])
async function resetIcon() { async function resetIcon() {

View File

@@ -37,6 +37,7 @@
(cat) => data.categories.includes(cat.name) && cat.project_type === 'mod' (cat) => data.categories.includes(cat.name) && cat.project_type === 'mod'
) )
" "
type="ignored"
> >
<EnvironmentIndicator <EnvironmentIndicator
:client-side="data.client_side" :client-side="data.client_side"
@@ -167,7 +168,7 @@
</Card> </Card>
</div> </div>
<div v-if="data" class="content-container"> <div v-if="data" class="content-container">
<Promotion /> <Promotion query-param="?r=launcher" />
<Card class="tabs"> <Card class="tabs">
<NavRow <NavRow
v-if="data.gallery.length > 0" v-if="data.gallery.length > 0"
@@ -205,6 +206,7 @@
:versions="versions" :versions="versions"
:members="members" :members="members"
:dependencies="dependencies" :dependencies="dependencies"
:instance="instance"
:install="install" :install="install"
:installed="installed" :installed="installed"
:installing="installing" :installing="installing"

View File

@@ -167,8 +167,8 @@ import { computed, ref, watch } from 'vue'
import { SwapIcon } from '@/assets/icons/index.js' import { SwapIcon } from '@/assets/icons/index.js'
const filterVersions = ref([]) const filterVersions = ref([])
const filterLoader = ref([]) const filterLoader = ref(props.instance ? [props.instance?.metadata?.loader] : [])
const filterGameVersions = ref([]) const filterGameVersions = ref(props.instance ? [props.instance?.metadata?.game_version] : [])
const currentPage = ref(1) const currentPage = ref(1)