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
*.pdb
theseus.iml

6
Cargo.lock generated
View File

@@ -4609,7 +4609,7 @@ dependencies = [
[[package]]
name = "theseus"
version = "0.5.0"
version = "0.5.1"
dependencies = [
"async-recursion",
"async-tungstenite",
@@ -4654,7 +4654,7 @@ dependencies = [
[[package]]
name = "theseus_cli"
version = "0.5.0"
version = "0.5.1"
dependencies = [
"argh",
"color-eyre",
@@ -4681,7 +4681,7 @@ dependencies = [
[[package]]
name = "theseus_gui"
version = "0.5.0"
version = "0.5.1"
dependencies = [
"chrono",
"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]
name = "theseus"
version = "0.5.0"
version = "0.5.1"
authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -2,6 +2,7 @@
<div
v-if="mode !== 'isolated'"
ref="button"
v-tooltip="'Minecraft accounts'"
class="button-base avatar-button"
:class="{ expanded: mode === 'expanded' }"
@click="showCard = !showCard"
@@ -14,15 +15,6 @@
: '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>
<transition name="fade">
<Card
@@ -64,10 +56,50 @@
</Button>
</Card>
</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>
<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 {
users,
@@ -76,10 +108,10 @@ import {
authenticate_await_completion,
} from '@/helpers/auth'
import { get, set } from '@/helpers/settings'
import { WebviewWindow } from '@tauri-apps/api/window'
import { handleError } from '@/store/state.js'
import { get as getCreds, login_minecraft } from '@/helpers/mr_auth'
import { mixpanel_track } from '@/helpers/mixpanel'
import QrcodeVue from 'qrcode.vue'
defineProps({
mode: {
@@ -93,6 +125,9 @@ const emit = defineEmits(['change'])
const settings = ref({})
const accounts = ref([])
const loginUrl = ref('')
const loginModal = ref(null)
async function refreshValues() {
settings.value = await get().catch(handleError)
accounts.value = await users().catch(handleError)
@@ -118,12 +153,18 @@ async function setAccount(account) {
async function login() {
const url = await authenticate_begin_flow().catch(handleError)
loginUrl.value = url
const window = new WebviewWindow('loginWindow', {
title: 'Modrinth App',
url: url,
await window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: url,
},
})
loginModal.value.show()
const loggedIn = await authenticate_await_completion().catch(handleError)
if (loggedIn && loggedIn[0]) {
@@ -139,7 +180,8 @@ async function login() {
}
}
}
await window.close()
loginModal.value.hide()
mixpanel_track('AccountLogIn')
}
@@ -329,4 +371,37 @@ onBeforeUnmount(() => {
gap: 0.25rem;
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>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
<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>
@@ -36,7 +40,14 @@
</template>
<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'
defineProps({
@@ -48,6 +59,10 @@ defineProps({
type: Boolean,
default: false,
},
exit: {
type: Function,
required: true,
},
})
</script>

View File

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

View File

@@ -63,7 +63,7 @@ defineProps({
>
<Avatar
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"
class="mod-image"
/>

View File

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

View File

@@ -8,7 +8,6 @@ import {
SettingsIcon,
XIcon,
Notifications,
LogOutIcon,
} from 'omorphia'
import { appWindow } from '@tauri-apps/api/window'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
@@ -160,7 +159,10 @@ onMounted(async () => {
<div class="btn icon-only" :class="{ active: phase < 4 }">
<HomeIcon />
</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 />
</div>
<div
@@ -175,9 +177,6 @@ onMounted(async () => {
</div>
</div>
<div class="settings pages-list">
<Button class="active" icon-only @click="finishOnboarding">
<LogOutIcon />
</Button>
<Button class="sleek-primary" icon-only>
<PlusIcon />
</Button>
@@ -192,7 +191,11 @@ onMounted(async () => {
<Breadcrumbs data-tauri-drag-region />
</section>
<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>
<TutorialTip
:progress-function="nextPhase"
@@ -430,12 +433,6 @@ onMounted(async () => {
background-color: var(--color-brand-highlight);
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 IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
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 { isOffline } from '@/helpers/utils'
import { offline_listener } from '@/helpers/events'
@@ -56,6 +56,7 @@ const orFacets = ref([])
const selectedVersions = ref([])
const onlyOpenSource = ref(false)
const showSnapshots = ref(false)
const hideAlreadyInstalled = ref(false)
const selectedEnvironments = ref([])
const sortTypes = readonly([
{ display: 'Relevance', name: 'relevance' },
@@ -143,6 +144,9 @@ if (route.query.m) {
if (route.query.o) {
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1
}
if (route.query.ai) {
hideAlreadyInstalled.value = route.query.ai === 'true'
}
async function refreshSearch() {
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)}`)
}
const offset = (currentPage.value - 1) * maxResults.value
@@ -339,6 +353,10 @@ function getSearchUrl(offset, useObj) {
queryItems.push('il=true')
obj.il = true
}
if (hideAlreadyInstalled.value) {
queryItems.push('ai=true')
obj.ai = true
}
let url = `${route.path}`
@@ -555,6 +573,13 @@ onUnmounted(() => unlistenOffline())
@update:model-value="onSearchChangeToTop(1)"
@click.prevent.stop
/>
<Checkbox
v-model="hideAlreadyInstalled"
label="Hide already installed"
class="filter-checkbox"
@update:model-value="onSearchChangeToTop(1)"
@click.prevent.stop
/>
</Card>
<Card class="search-panel-card">
<Button
@@ -662,7 +687,7 @@ onUnmounted(() => unlistenOffline())
</Card>
</aside>
<div class="search">
<Promotion class="promotion" :external="false" />
<Promotion class="promotion" :external="false" query-param="?r=launcher" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" />
</Card>
@@ -711,7 +736,7 @@ onUnmounted(() => unlistenOffline())
@switch-page="onSearchChange"
/>
<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!
</section>
<section v-else class="project-list display-mode--list instance-results" role="list">

View File

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

View File

@@ -308,7 +308,8 @@ async function refreshDir() {
<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.
customize your experience. By enabling this option, you opt out and your data will no
longer be collected.
</span>
</label>
<Toggle

View File

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

View File

@@ -1,26 +1,13 @@
<template>
<Card
v-if="projects.length > 0"
class="mod-card"
:class="{ static: instance.metadata.linked_data }"
>
<div class="second-row">
<Chips
v-if="Object.keys(selectableProjectTypes).length > 1"
<Card v-if="projects.length > 0" class="mod-card">
<div class="dropdown-input">
<DropdownSelect
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">
<SearchIcon />
<input
@@ -37,228 +24,253 @@
<XIcon />
</Button>
</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 class="table">
<div class="table-row table-head" :class="{ 'show-options': selected.length > 0 }">
<div v-if="!instance.metadata.linked_data" class="table-cell table-text">
<Checkbox v-model="selectAll" class="select-checkbox" />
</div>
<div v-if="selected.length === 0" class="table-cell table-text name-cell actions-cell">
<Button class="transparent" @click="sortProjects('Name')">
Name
<DropdownIcon v-if="sortColumn === 'Name'" :class="{ down: ascending }" />
</Button>
</div>
<div v-if="selected.length === 0" class="table-cell table-text version">
<Button class="transparent" @click="sortProjects('Version')">
Version
<DropdownIcon v-if="sortColumn === 'Version'" :class="{ down: ascending }" />
</Button>
</div>
<div v-if="selected.length === 0" class="table-cell table-text actions-cell">
<Button
v-if="!instance.metadata.linked_data"
class="transparent"
@click="sortProjects('Enabled')"
>
Actions
<DropdownIcon v-if="sortColumn === 'Enabled'" :class="{ down: ascending }" />
</Button>
</div>
<div v-else-if="!instance.metadata.linked_data" class="options table-cell name-cell">
<Button
class="transparent share"
@click="() => (showingOptions = !showingOptions)"
@mouseover="selectedOption = 'Share'"
>
<MenuIcon :class="{ open: showingOptions }" />
</Button>
<Button
class="transparent share"
@click="shareNames()"
@mouseover="selectedOption = 'Share'"
>
<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>
<Button
v-if="isPackLinked"
v-tooltip="'Modpack is up to date'"
:disabled="updatingModpack || !canUpdatePack"
color="secondary"
@click="updateModpack"
>
<UpdatedIcon />
{{ updatingModpack ? 'Updating' : 'Update modpack' }}
</Button>
<Button v-else @click="exportModal.show()">
<PackageIcon />
Export modpack
</Button>
<DropdownButton
v-if="!isPackLinked"
: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>
</Card>
<Pagination
v-if="projects.length > 0"
:page="currentPage"
:count="Math.ceil(search.length / 20)"
class="pagination-before"
:link-function="(page) => `?page=${page}`"
@switch-page="switchPage"
/>
<Card
v-if="projects.length > 0"
class="list-card"
:class="{ static: instance.metadata.linked_data }"
>
<div class="table">
<div class="table-row table-head" :class="{ 'show-options': selected.length > 0 }">
<div v-if="!instance.metadata.linked_data" class="table-cell table-text">
<Checkbox v-model="selectAll" class="select-checkbox" />
</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 v-if="selected.length === 0" class="table-cell table-text name-cell actions-cell">
<Button class="transparent" @click="sortProjects('Name')">
Name
<DropdownIcon v-if="sortColumn === 'Name'" :class="{ down: ascending }" />
</Button>
</div>
<div
v-for="mod in search"
: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>
</router-link>
<div v-else class="mod-content">
<Avatar :src="mod.icon" />
<span v-tooltip="`${mod.name}`" class="title">{{ mod.name }}</span>
<div v-if="selected.length === 0" class="table-cell table-text version">
<Button class="transparent" @click="sortProjects('Version')">
Version
<DropdownIcon v-if="sortColumn === 'Version'" :class="{ down: ascending }" />
</Button>
</div>
<div v-if="selected.length === 0" class="table-cell table-text actions-cell">
<Button
v-if="!instance.metadata.linked_data"
class="transparent"
@click="sortProjects('Enabled')"
>
Actions
<DropdownIcon v-if="sortColumn === 'Enabled'" :class="{ down: ascending }" />
</Button>
</div>
<div v-else-if="!instance.metadata.linked_data" class="options table-cell name-cell">
<Button
class="transparent share"
@click="() => (showingOptions = !showingOptions)"
@mouseover="selectedOption = 'Share'"
>
<MenuIcon :class="{ open: showingOptions }" />
</Button>
<Button
class="transparent share"
@click="shareNames()"
@mouseover="selectedOption = 'Share'"
>
<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>
</router-link>
<div v-else class="mod-content">
<Avatar :src="mod.icon" />
<span v-tooltip="`${mod.name}`" class="title">{{ mod.name }}</span>
</div>
<div class="table-cell table-text version">
<span v-tooltip="`${mod.version}`">{{ mod.version }}</span>
</div>
<div class="table-cell table-text manage">
<Button
v-if="!instance.metadata.linked_data"
v-tooltip="'Remove project'"
icon-only
@click="removeMod(mod)"
>
<TrashIcon />
</Button>
<AnimatedLogo
v-if="mod.updating && !instance.metadata.linked_data"
class="btn icon-only updating-indicator"
></AnimatedLogo>
<Button
v-else-if="!instance.metadata.linked_data"
v-tooltip="'Update project'"
:disabled="!mod.outdated || offline"
icon-only
@click="updateProject(mod)"
>
<UpdatedIcon v-if="mod.outdated" />
<CheckIcon v-else />
</Button>
<input
v-if="!instance.metadata.linked_data"
id="switch-1"
autocomplete="off"
type="checkbox"
class="switch stylized-toggle"
:checked="!mod.disabled"
@change="toggleDisableMod(mod)"
/>
<Button
v-tooltip="`Show ${mod.file_name}`"
icon-only
@click="showProfileInFolder(mod.path)"
>
<FolderOpenIcon />
</Button>
</div>
</div>
<div class="table-cell table-text version">
<span v-tooltip="`${mod.version}`">{{ mod.version }}</span>
</div>
<div class="table-cell table-text manage">
<Button
v-if="!instance.metadata.linked_data"
v-tooltip="'Remove project'"
icon-only
@click="removeMod(mod)"
>
<TrashIcon />
</Button>
<AnimatedLogo
v-if="mod.updating && !instance.metadata.linked_data"
class="btn icon-only updating-indicator"
></AnimatedLogo>
<Button
v-else-if="!instance.metadata.linked_data"
v-tooltip="'Update project'"
:disabled="!mod.outdated || offline"
icon-only
@click="updateProject(mod)"
>
<UpdatedIcon v-if="mod.outdated" />
<CheckIcon v-else />
</Button>
<input
v-if="!instance.metadata.linked_data"
id="switch-1"
autocomplete="off"
type="checkbox"
class="switch stylized-toggle"
:checked="!mod.disabled"
@change="toggleDisableMod(mod)"
/>
<Button
v-tooltip="`Show ${mod.file_name}`"
icon-only
@click="showProfileInFolder(mod.path)"
>
<FolderOpenIcon />
</Button>
</div>
</div>
</div>
@@ -335,6 +347,7 @@
share-title="Sharing modpack content"
share-text="Check out the projects I'm using in my modpack!"
/>
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
</template>
<script setup>
import {
@@ -346,7 +359,6 @@ import {
SearchIcon,
UpdatedIcon,
AnimatedLogo,
Chips,
FolderOpenIcon,
Checkbox,
formatProjectType,
@@ -361,6 +373,8 @@ import {
EyeOffIcon,
ShareModal,
CodeIcon,
Pagination,
DropdownSelect,
} from 'omorphia'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
@@ -379,7 +393,8 @@ import { open } from '@tauri-apps/api/dialog'
import { listen } from '@tauri-apps/api/event'
import { convertFileSrc } from '@tauri-apps/api/tauri'
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()
@@ -407,12 +422,13 @@ const props = defineProps({
const projects = ref([])
const selectionMap = ref(new Map())
const showingOptions = ref(false)
const canUpdatePack = computed(() => {
return (
props.instance.metadata.linked_data &&
props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
)
const isPackLinked = computed(() => {
return props.instance.metadata.linked_data
})
const canUpdatePack = computed(() => {
return props.instance.metadata.linked_data.version_id !== props.instance.modrinth_update_version
})
const exportModal = ref(null)
console.log(props.instance)
const initProjects = (initInstance) => {
@@ -501,6 +517,7 @@ const selectedOption = ref('Share')
const shareModal = ref(null)
const ascending = ref(true)
const sortColumn = ref('Name')
const currentPage = ref(1)
const selected = computed(() =>
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))
})
const switchPage = (page) => {
currentPage.value = page
}
onUnmounted(() => {
unlisten()
})
@@ -903,10 +924,56 @@ onUnmounted(() => {
.mod-card {
display: flex;
flex-direction: column;
flex-direction: row;
flex-wrap: wrap;
gap: var(--gap-sm);
justify-content: center;
overflow: hidden;
justify-content: flex-start;
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 {
@@ -1078,4 +1145,10 @@ onUnmounted(() => {
margin: 0;
}
}
.dropdown-input {
.selected {
height: 2.5rem;
}
}
</style>

View File

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

View File

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

View File

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