You've already forked AstralRinth
forked from didirus/AstralRinth
App redesign (#2946)
* Start of app redesign * format * continue progress * Content page nearly done * Fix recursion issues with content page * Fix update all alignment * Discover page progress * Settings progress * Removed unlocked-size hack that breaks web * Revamp project page, refactor web project page to share code with app, fixed loading bar, misc UI/UX enhancements, update ko-fi logo, update arrow icons, fix web issues caused by floating-vue migration, fix tooltip issues, update web tooltips, clean up web hydration issues * Ads + run prettier * Begin auth refactor, move common messages to ui lib, add i18n extraction to all apps, begin Library refactor * fix ads not hiding when plus log in * rev lockfile changes/conflicts * Fix sign in page * Add generated * (mostly) Data driven search * Fix search mobile issue * profile fixes * Project versions page, fix typescript on UI lib and misc fixes * Remove unused gallery component * Fix linkfunction err * Search filter controls at top, localization for locked filters * Fix provided filter names * Fix navigating from instance browse to main browse * Friends frontend (#2995) * Friends system frontend * (almost) finish frontend * finish friends, fix lint * Fix lint --------- Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com> * Refresh macOS app icon * Update web search UI more * Fix link opens * Fix frontend build --------- Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
@@ -1,85 +1,107 @@
|
||||
<template>
|
||||
<div class="instance-container">
|
||||
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()">
|
||||
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ instance.name }}</h2>
|
||||
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
|
||||
<div
|
||||
class="p-6 pr-2 pb-4"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||
>
|
||||
<ExportModal ref="exportModal" :instance="instance" />
|
||||
<ContentPageHeader>
|
||||
<template #icon>
|
||||
<Avatar :src="icon" :alt="instance.name" size="96px" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ instance.name }}
|
||||
</template>
|
||||
<template #summary> </template>
|
||||
<template #stats>
|
||||
<div class="flex items-center gap-2 font-semibold transform capitalize">
|
||||
<GameIcon class="h-6 w-6 text-secondary" />
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</div>
|
||||
<span class="button-group">
|
||||
<Button v-if="instance.install_stage !== 'installed'" disabled class="instance-button">
|
||||
Installing...
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="playing === true"
|
||||
color="danger"
|
||||
class="instance-button"
|
||||
@click="stopInstance('InstancePage')"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="playing === false && loading === false"
|
||||
color="primary"
|
||||
class="instance-button"
|
||||
@click="startInstance('InstancePage')"
|
||||
>
|
||||
<PlayIcon />
|
||||
Play
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="loading === true && playing === false"
|
||||
disabled
|
||||
class="instance-button"
|
||||
>
|
||||
Loading...
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip="'Open instance folder'"
|
||||
class="instance-button"
|
||||
@click="showProfileInFolder(instance.path)"
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
Folder
|
||||
</Button>
|
||||
</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="pages-list">
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/`" class="btn">
|
||||
<BoxIcon />
|
||||
Content
|
||||
</RouterLink>
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/logs`" class="btn">
|
||||
<FileIcon />
|
||||
Logs
|
||||
</RouterLink>
|
||||
<RouterLink :to="`/instance/${encodeURIComponent($route.params.id)}/options`" class="btn">
|
||||
<SettingsIcon />
|
||||
Options
|
||||
</RouterLink>
|
||||
</div>
|
||||
</Card>
|
||||
<PromotionWrapper ref="promo" class="mt-4" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ButtonStyled v-if="instance.install_stage !== 'installed'" color="brand" size="large">
|
||||
<button disabled>Installing...</button>
|
||||
</ButtonStyled>
|
||||
<template v-else>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled v-if="playing === true" color="red" size="large">
|
||||
<button @click="stopInstance('InstancePage')">
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="playing === false && loading === false"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button @click="startInstance('InstancePage')">
|
||||
<PlayIcon />
|
||||
Play
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="loading === true && playing === false"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button disabled>Loading...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular>
|
||||
<RouterLink
|
||||
v-tooltip="'Instance settings'"
|
||||
:to="`/instance/${encodeURIComponent(route.params.id)}/options`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</RouterLink>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => showProfileInFolder(instance.path),
|
||||
},
|
||||
{
|
||||
id: 'export-mrpack',
|
||||
action: () => $refs.exportModal.show(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #share-instance> <UserPlusIcon /> Share instance </template>
|
||||
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</div>
|
||||
<div class="px-6">
|
||||
<NavTabs :links="tabs" />
|
||||
</div>
|
||||
<div class="p-6 pt-4">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
:options="options"
|
||||
:offline="offline"
|
||||
:playing="playing"
|
||||
:versions="modrinthVersions"
|
||||
:installed="instance.install_stage !== 'installed'"
|
||||
></component>
|
||||
<template #fallback>
|
||||
<LoadingIndicator />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
@@ -104,11 +126,18 @@
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, Avatar, Card } from '@modrinth/ui'
|
||||
import {
|
||||
BoxIcon,
|
||||
Avatar,
|
||||
ContentPageHeader,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
LoadingIndicator,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
UserPlusIcon,
|
||||
ServerIcon,
|
||||
PackageIcon,
|
||||
SettingsIcon,
|
||||
FileIcon,
|
||||
PlayIcon,
|
||||
StopCircleIcon,
|
||||
EditIcon,
|
||||
@@ -122,21 +151,24 @@ import {
|
||||
XIcon,
|
||||
CheckCircleIcon,
|
||||
UpdatedIcon,
|
||||
MoreVerticalIcon,
|
||||
GameIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { get, kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ref, onUnmounted, computed } from 'vue'
|
||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||
import dayjs from 'dayjs'
|
||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -145,6 +177,17 @@ const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const instance = ref(await get(route.params.id).catch(handleError))
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
label: 'Content',
|
||||
href: `/instance/${encodeURIComponent(route.params.id)}`,
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
href: `/instance/${encodeURIComponent(route.params.id)}/logs`,
|
||||
},
|
||||
])
|
||||
|
||||
breadcrumbs.setName(
|
||||
'Instance',
|
||||
instance.value.name.length > 40
|
||||
@@ -300,6 +343,10 @@ const unlistenProcesses = await process_listener((e) => {
|
||||
if (e.event === 'finished' && e.profile_path_id === route.params.id) playing.value = false
|
||||
})
|
||||
|
||||
const icon = computed(() =>
|
||||
instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenProfiles()
|
||||
|
||||
@@ -1,331 +1,227 @@
|
||||
<template>
|
||||
<Card v-if="projects.length > 0" class="mod-card">
|
||||
<div class="dropdown-input">
|
||||
<DropdownSelect
|
||||
v-model="selectedProjectType"
|
||||
:options="Object.keys(selectableProjectTypes)"
|
||||
default-value="All"
|
||||
name="project-type-dropdown"
|
||||
color="primary"
|
||||
/>
|
||||
<div class="iconified-input">
|
||||
<template v-if="projects.length > 0">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="iconified-input flex-grow">
|
||||
<SearchIcon />
|
||||
<input
|
||||
v-model="searchFilter"
|
||||
type="text"
|
||||
:placeholder="`Search ${search.length} ${(['All', 'Other'].includes(selectedProjectType)
|
||||
? 'projects'
|
||||
: selectedProjectType.toLowerCase()
|
||||
).slice(0, search.length === 1 ? -1 : 64)}...`"
|
||||
class="text-input"
|
||||
:placeholder="`Search content...`"
|
||||
class="text-input search-input"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (searchFilter = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<AddContentButton :instance="instance" />
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="'Refresh projects'"
|
||||
icon-only
|
||||
:disabled="refreshingProjects"
|
||||
@click="refreshProjects"
|
||||
>
|
||||
<UpdatedIcon />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="canUpdatePack"
|
||||
:disabled="installing"
|
||||
color="secondary"
|
||||
@click="modpackVersionModal.show()"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ installing ? 'Updating' : 'Update modpack' }}
|
||||
</Button>
|
||||
<Button v-else-if="!isPackLocked" @click="exportModal.show()">
|
||||
<PackageIcon />
|
||||
Export modpack
|
||||
</Button>
|
||||
<Button v-if="!isPackLocked && projects.some((m) => m.outdated)" @click="updateAll">
|
||||
<DownloadIcon />
|
||||
Update all
|
||||
</Button>
|
||||
<AddContentButton v-if="!isPackLocked" :instance="instance" />
|
||||
</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">
|
||||
<div class="table">
|
||||
<div class="table-row table-head" :class="{ 'show-options': selected.length > 0 }">
|
||||
<div 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 class="transparent" @click="sortProjects('Enabled')">
|
||||
Actions
|
||||
<DropdownIcon v-if="sortColumn === 'Enabled'" :class="{ down: ascending }" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="options table-cell name-cell">
|
||||
<div>
|
||||
<Button
|
||||
class="transparent share"
|
||||
@click="() => (showingOptions = !showingOptions)"
|
||||
@mouseover="selectedOption = 'Share'"
|
||||
>
|
||||
<MenuIcon :class="{ open: showingOptions }" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
class="transparent share"
|
||||
@click="shareNames()"
|
||||
@mouseover="selectedOption = 'Share'"
|
||||
>
|
||||
<ShareIcon />
|
||||
Share
|
||||
</Button>
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to remove mods' : ''">
|
||||
<Button
|
||||
:disabled="isPackLocked"
|
||||
class="transparent trash"
|
||||
@click="deleteWarning.show()"
|
||||
@mouseover="selectedOption = 'Delete'"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to update mods' : ''">
|
||||
<Button
|
||||
:disabled="isPackLocked || offline"
|
||||
class="transparent update"
|
||||
@click="updateSelected()"
|
||||
@mouseover="selectedOption = 'Update'"
|
||||
>
|
||||
<UpdatedIcon />
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to toggle mods' : ''">
|
||||
<Button
|
||||
:disabled="isPackLocked"
|
||||
class="transparent"
|
||||
@click="toggleSelected()"
|
||||
@mouseover="selectedOption = 'Toggle'"
|
||||
>
|
||||
<ToggleIcon />
|
||||
Toggle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showingOptions && selected.length > 0" 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="filterOptions.length > 1" class="flex flex-wrap gap-1 items-center pb-4">
|
||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||
<button
|
||||
v-for="filter in filterOptions"
|
||||
:key="filter"
|
||||
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
||||
@click="toggleArray(selectedFilters, filter.id)"
|
||||
>
|
||||
<div 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"
|
||||
{{ filter.formattedName }}
|
||||
</button>
|
||||
</div>
|
||||
<ContentListPanel
|
||||
v-model="selectedFiles"
|
||||
:locked="isPackLocked"
|
||||
:items="
|
||||
search.map((x) => {
|
||||
const item: ContentItem<any> = {
|
||||
path: x.path,
|
||||
disabled: x.disabled,
|
||||
filename: x.file_name,
|
||||
icon: x.icon,
|
||||
title: x.name,
|
||||
data: x,
|
||||
}
|
||||
|
||||
if (x.version) {
|
||||
item.version = x.version
|
||||
item.versionId = x.version
|
||||
}
|
||||
|
||||
if (x.id) {
|
||||
item.project = {
|
||||
id: x.id,
|
||||
link: { path: `/project/${x.id}`, query: { i: props.instance.path } },
|
||||
linkProps: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (x.author) {
|
||||
item.creator = {
|
||||
name: x.author,
|
||||
type: 'user',
|
||||
id: x.author,
|
||||
link: 'https://modrinth.com/user/' + x.author,
|
||||
linkProps: { target: '_blank' },
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
"
|
||||
:sort-column="sortColumn"
|
||||
:sort-ascending="ascending"
|
||||
:update-sort="sortProjects"
|
||||
>
|
||||
<template v-if="selectedProjects.length > 0" #headers>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="!isPackLocked && selectedProjects.some((m) => m.outdated)"
|
||||
color="brand"
|
||||
color-fill="text"
|
||||
hover-color-fill="text"
|
||||
>
|
||||
<Avatar :src="mod.icon" />
|
||||
<div class="mod-text">
|
||||
<div class="title">{{ mod.name }}</div>
|
||||
<span v-if="mod.author" 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>
|
||||
<div class="table-cell table-text version">
|
||||
<span v-tooltip="`${mod.version}`">{{ mod.version }}</span>
|
||||
</div>
|
||||
<div class="table-cell table-text manage">
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to remove mods.' : 'Remove project'">
|
||||
<Button :disabled="isPackLocked" icon-only @click="removeMod(mod)">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator" />
|
||||
<div
|
||||
v-else
|
||||
v-tooltip="isPackLocked ? 'Unlock this instance to update mods.' : 'Update project'"
|
||||
>
|
||||
<Button
|
||||
:disabled="!mod.outdated || offline || isPackLocked"
|
||||
icon-only
|
||||
@click="updateProject(mod)"
|
||||
<button @click="updateSelected()"><DownloadIcon /> Update</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'share-names',
|
||||
action: () => shareNames(),
|
||||
},
|
||||
{
|
||||
id: 'share-file-names',
|
||||
action: () => shareFileNames(),
|
||||
},
|
||||
{
|
||||
id: 'share-urls',
|
||||
action: () => shareUrls(),
|
||||
},
|
||||
{
|
||||
id: 'share-markdown',
|
||||
action: () => shareMarkdown(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<UpdatedIcon v-if="mod.outdated" />
|
||||
<CheckIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-tooltip="isPackLocked ? 'Unlock this instance to toggle mods.' : ''">
|
||||
<input
|
||||
id="switch-1"
|
||||
:disabled="isPackLocked"
|
||||
autocomplete="off"
|
||||
type="checkbox"
|
||||
class="switch stylized-toggle"
|
||||
:checked="!mod.disabled"
|
||||
@change="toggleDisableMod(mod)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="`Show ${mod.file_name}`"
|
||||
icon-only
|
||||
@click="highlightModInProfile(instance.path, mod.path)"
|
||||
>
|
||||
<FolderOpenIcon />
|
||||
</Button>
|
||||
<ShareIcon /> Share <DropdownIcon />
|
||||
<template #share-names> <TextInputIcon /> Project names </template>
|
||||
<template #share-file-names> <FileIcon /> File names </template>
|
||||
<template #share-urls> <LinkIcon /> Project links </template>
|
||||
<template #share-markdown> <CodeIcon /> Markdown links </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="selectedProjects.some((m) => m.disabled)">
|
||||
<button @click="enableAll()"><CheckCircleIcon /> Enable</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="selectedProjects.some((m) => !m.disabled)">
|
||||
<button @click="disableAll()"><SlashIcon /> Disable</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="deleteSelected()"><TrashIcon /> Remove</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div v-else class="empty-prompt">
|
||||
<div class="empty-icon">
|
||||
<AddProjectImage />
|
||||
</div>
|
||||
<h3>No projects found</h3>
|
||||
<p class="empty-subtitle">Add a project to get started</p>
|
||||
<AddContentButton :instance="instance" />
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="projects.length > 0"
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(search.length / 20)"
|
||||
class="pagination-after"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<ModalWrapper ref="deleteWarning" header="Are you sure?">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Are you sure you want to remove
|
||||
<strong>{{ functionValues.length }} project(s)</strong> from {{ instance.name }}?
|
||||
<br />
|
||||
This action <strong>cannot</strong> be undone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="button-group push-right">
|
||||
<Button @click="deleteWarning.hide()"> Cancel </Button>
|
||||
<Button color="danger" @click="deleteSelected">
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<ModalWrapper ref="deleteDisabledWarning" header="Are you sure?">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Are you sure you want to remove
|
||||
<strong
|
||||
>{{ Array.from(projects.values()).filter((x) => x.disabled).length }} disabled
|
||||
project(s)</strong
|
||||
</template>
|
||||
<template #header-actions>
|
||||
<ButtonStyled type="transparent" color-fill="text" hover-color-fill="text">
|
||||
<button :disabled="refreshingProjects" class="w-max" @click="refreshProjects">
|
||||
<UpdatedIcon />
|
||||
Refresh
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="!isPackLocked && projects.some((m) => (m as any).outdated)"
|
||||
type="transparent"
|
||||
color="brand"
|
||||
color-fill="text"
|
||||
hover-color-fill="text"
|
||||
@click="updateAll"
|
||||
>
|
||||
<button class="w-max"><DownloadIcon /> Update all</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="canUpdatePack"
|
||||
type="transparent"
|
||||
color="brand"
|
||||
color-fill="text"
|
||||
hover-color-fill="text"
|
||||
>
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal.show()">
|
||||
<DownloadIcon /> Update pack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<ButtonStyled
|
||||
v-if="!isPackLocked && (item.data as any).outdated"
|
||||
type="transparent"
|
||||
color="brand"
|
||||
circular
|
||||
>
|
||||
<button
|
||||
v-tooltip="`Update`"
|
||||
:disabled="(item.data as any).updating"
|
||||
@click="updateProject(item.data)"
|
||||
>
|
||||
from {{ instance.name }}?
|
||||
<br />
|
||||
This action <strong>cannot</strong> be undone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="button-group push-right">
|
||||
<Button @click="deleteDisabledWarning.hide()"> Cancel </Button>
|
||||
<Button color="danger" @click="deleteDisabled">
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</Button>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-else class="w-[36px]"></div>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button
|
||||
v-tooltip="item.disabled ? `Enable` : `Disable`"
|
||||
@click="toggleDisableMod(item.data)"
|
||||
>
|
||||
<CheckCircleIcon v-if="item.disabled" />
|
||||
<SlashIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'show-file',
|
||||
action: () => highlightModInProfile(instance.path, item.path),
|
||||
},
|
||||
{
|
||||
id: 'copy-link',
|
||||
shown: item.project !== undefined,
|
||||
action: () => toggleDisableMod(item.data),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'remove',
|
||||
color: 'red',
|
||||
action: () => removeMod(item),
|
||||
},
|
||||
]"
|
||||
direction="left"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #show-file> <ExternalIcon /> Show file </template>
|
||||
<template #copy-link> <ClipboardCopyIcon /> Copy link </template>
|
||||
<template v-if="item.disabled" #toggle> <CheckCircleIcon /> Enable </template>
|
||||
<template v-else #toggle> <SlashIcon /> Disable </template>
|
||||
<template #remove> <TrashIcon /> Remove </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentListPanel>
|
||||
</template>
|
||||
<div v-else class="w-full flex flex-col items-center justify-center mt-6 max-w-[48rem] mx-auto">
|
||||
<div class="top-box w-full">
|
||||
<div class="flex items-center gap-6 w-[32rem] mx-auto">
|
||||
<img src="@/assets/sad-modrinth-bot.webp" class="h-24" />
|
||||
<span class="text-contrast font-bold text-xl"
|
||||
>You haven't added any content to this instance yet.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<div class="top-box-divider"></div>
|
||||
<div class="flex items-center gap-6 py-4">
|
||||
<AddContentButton :instance="instance" />
|
||||
</div>
|
||||
</div>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
share-title="Sharing modpack content"
|
||||
@@ -340,34 +236,30 @@
|
||||
:versions="props.versions"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ExternalIcon,
|
||||
LinkIcon,
|
||||
ClipboardCopyIcon,
|
||||
TrashIcon,
|
||||
CheckIcon,
|
||||
SearchIcon,
|
||||
UpdatedIcon,
|
||||
FolderOpenIcon,
|
||||
XIcon,
|
||||
ShareIcon,
|
||||
DropdownIcon,
|
||||
GlobeIcon,
|
||||
FileIcon,
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
CodeIcon,
|
||||
DownloadIcon,
|
||||
FilterIcon,
|
||||
MoreVerticalIcon,
|
||||
CheckCircleIcon,
|
||||
SlashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Pagination,
|
||||
DropdownSelect,
|
||||
Checkbox,
|
||||
AnimatedLogo,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
} from '@modrinth/ui'
|
||||
import { Button, ButtonStyled, ContentListPanel, OverflowMenu } from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useVIntl, defineMessages } from '@vintl/vintl'
|
||||
import {
|
||||
add_project_from_path,
|
||||
get_projects,
|
||||
@@ -379,7 +271,7 @@ import {
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { highlightModInProfile } from '@/helpers/utils.js'
|
||||
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
|
||||
import { TextInputIcon } from '@/assets/icons'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
import AddContentButton from '@/components/ui/AddContentButton.vue'
|
||||
@@ -390,9 +282,9 @@ import {
|
||||
get_version_many,
|
||||
} from '@/helpers/cache.js'
|
||||
import { profile_listener } from '@/helpers/events.js'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
@@ -433,7 +325,6 @@ onUnmounted(() => {
|
||||
unlistenProfiles()
|
||||
})
|
||||
|
||||
const showingOptions = ref(false)
|
||||
const isPackLocked = computed(() => {
|
||||
return props.instance.linked_data && props.instance.linked_data.locked
|
||||
})
|
||||
@@ -444,9 +335,14 @@ const canUpdatePack = computed(() => {
|
||||
const exportModal = ref(null)
|
||||
|
||||
const projects = ref([])
|
||||
const selectedFiles = ref([])
|
||||
const selectedProjects = computed(() =>
|
||||
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
|
||||
)
|
||||
|
||||
const selectionMap = ref(new Map())
|
||||
|
||||
const initProjects = async (cacheBehaviour) => {
|
||||
const initProjects = async (cacheBehaviour?) => {
|
||||
const newProjects = []
|
||||
|
||||
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
|
||||
@@ -504,6 +400,7 @@ const initProjects = async (cacheBehaviour) => {
|
||||
icon: project.icon_url,
|
||||
disabled: file.file_name.endsWith('.disabled'),
|
||||
updateVersion: file.update_version_id,
|
||||
updated: dayjs(version.date_published),
|
||||
outdated: !!file.update_version_id,
|
||||
project_type: project.project_type,
|
||||
id: project.id,
|
||||
@@ -545,19 +442,77 @@ await initProjects()
|
||||
const modpackVersionModal = ref(null)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const vintl = useVIntl()
|
||||
const { formatMessage } = vintl
|
||||
|
||||
type FilterOption = {
|
||||
id: string
|
||||
formattedName: string
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
updatesAvailableFilter: {
|
||||
id: 'instance.filter.updates-available',
|
||||
defaultMessage: 'Updates available',
|
||||
},
|
||||
})
|
||||
|
||||
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
const options: FilterOption[] = []
|
||||
|
||||
const frequency = projects.value.reduce((map, item) => {
|
||||
map[item.project_type] = (map[item.project_type] || 0) + 1
|
||||
return map
|
||||
}, {})
|
||||
|
||||
const types = Object.keys(frequency).sort((a, b) => frequency[b] - frequency[a])
|
||||
|
||||
types.forEach((type) => {
|
||||
options.push({
|
||||
id: type,
|
||||
formattedName: formatProjectType(type) + 's',
|
||||
})
|
||||
})
|
||||
|
||||
if (!isPackLocked.value && projects.value.some((m) => m.outdated)) {
|
||||
options.push({
|
||||
id: 'updates',
|
||||
formattedName: formatMessage(messages.updatesAvailableFilter),
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const selectedFilters = ref([])
|
||||
const filteredProjects = computed(() => {
|
||||
const updatesFilter = selectedFilters.value.includes('updates')
|
||||
|
||||
const typeFilters = selectedFilters.value.filter((filter) => filter !== 'updates')
|
||||
|
||||
return projects.value.filter((project) => {
|
||||
return (
|
||||
(typeFilters.length === 0 || typeFilters.includes(project.project_type)) &&
|
||||
(!updatesFilter || project.outdated)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function toggleArray(array, value) {
|
||||
if (array.includes(value)) {
|
||||
array.splice(array.indexOf(value), 1)
|
||||
} else {
|
||||
array.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
const searchFilter = ref('')
|
||||
const selectAll = ref(false)
|
||||
const selectedProjectType = ref('All')
|
||||
const deleteWarning = ref(null)
|
||||
const deleteDisabledWarning = ref(null)
|
||||
const hideNonSelected = ref(false)
|
||||
const selectedOption = ref('Share')
|
||||
const shareModal = ref(null)
|
||||
const ascending = ref(true)
|
||||
const sortColumn = ref('Name')
|
||||
const currentPage = ref(1)
|
||||
|
||||
watch([searchFilter, selectedProjectType], () => (currentPage.value = 1))
|
||||
|
||||
const selected = computed(() =>
|
||||
Array.from(selectionMap.value)
|
||||
@@ -570,7 +525,7 @@ const selected = computed(() =>
|
||||
)
|
||||
|
||||
const functionValues = computed(() =>
|
||||
selected.value.length > 0 ? selected.value : Array.from(projects.value.values()),
|
||||
selectedProjects.value.length > 0 ? selectedProjects.value : Array.from(projects.value.values()),
|
||||
)
|
||||
|
||||
const selectableProjectTypes = computed(() => {
|
||||
@@ -586,7 +541,7 @@ const selectableProjectTypes = computed(() => {
|
||||
|
||||
const search = computed(() => {
|
||||
const projectType = selectableProjectTypes.value[selectedProjectType.value]
|
||||
const filtered = projects.value
|
||||
const filtered = filteredProjects.value
|
||||
.filter((mod) => {
|
||||
return (
|
||||
mod.name.toLowerCase().includes(searchFilter.value.toLowerCase()) &&
|
||||
@@ -600,43 +555,19 @@ const search = computed(() => {
|
||||
return true
|
||||
})
|
||||
|
||||
return updateSort(filtered)
|
||||
})
|
||||
|
||||
const updateSort = (projects) => {
|
||||
switch (sortColumn.value) {
|
||||
case 'Version':
|
||||
return projects.slice().sort((a, b) => {
|
||||
if (a.version < b.version) {
|
||||
return ascending.value ? -1 : 1
|
||||
}
|
||||
if (a.version > b.version) {
|
||||
case 'Updated':
|
||||
return filtered.slice().sort((a, b) => {
|
||||
if (a.updated < b.updated) {
|
||||
return ascending.value ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
case 'Author':
|
||||
return projects.slice().sort((a, b) => {
|
||||
if (a.author < b.author) {
|
||||
return ascending.value ? -1 : 1
|
||||
}
|
||||
if (a.author > b.author) {
|
||||
return ascending.value ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
case 'Enabled':
|
||||
return projects.slice().sort((a, b) => {
|
||||
if (a.disabled && !b.disabled) {
|
||||
return ascending.value ? 1 : -1
|
||||
}
|
||||
if (!a.disabled && b.disabled) {
|
||||
if (a.updated > b.updated) {
|
||||
return ascending.value ? -1 : 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
default:
|
||||
return projects.slice().sort((a, b) => {
|
||||
return filtered.slice().sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return ascending.value ? -1 : 1
|
||||
}
|
||||
@@ -646,7 +577,7 @@ const updateSort = (projects) => {
|
||||
return 0
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const sortProjects = (filter) => {
|
||||
if (sortColumn.value === filter) {
|
||||
@@ -690,14 +621,6 @@ const updateAll = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const selectUpdatable = () => {
|
||||
for (const project of projects.value) {
|
||||
if (project.outdated) {
|
||||
selectionMap.value.set(project.path, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateProject = async (mod) => {
|
||||
mod.updating = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
@@ -753,6 +676,7 @@ const toggleDisableMod = async (mod) => {
|
||||
}
|
||||
|
||||
const removeMod = async (mod) => {
|
||||
console.log(mod)
|
||||
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
||||
|
||||
@@ -771,20 +695,9 @@ const deleteSelected = async () => {
|
||||
}
|
||||
|
||||
projects.value = projects.value.filter((x) => !x.selected)
|
||||
deleteWarning.value.hide()
|
||||
}
|
||||
|
||||
const deleteDisabled = async () => {
|
||||
for (const project of Array.of(projects.value.values().filter((x) => x.disabled))) {
|
||||
await remove_project(props.instance.path, project.path).catch(handleError)
|
||||
}
|
||||
|
||||
projects.value = projects.value.filter((x) => !x.selected)
|
||||
deleteDisabledWarning.value.hide()
|
||||
}
|
||||
|
||||
const shareNames = async () => {
|
||||
console.log(functionValues.value)
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
}
|
||||
|
||||
@@ -814,12 +727,6 @@ const shareMarkdown = async () => {
|
||||
)
|
||||
}
|
||||
|
||||
const toggleSelected = async () => {
|
||||
for (const project of functionValues.value) {
|
||||
await toggleDisableMod(project, !project.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
const updateSelected = async () => {
|
||||
const promises = []
|
||||
for (const project of functionValues.value) {
|
||||
@@ -829,35 +736,23 @@ const updateSelected = async () => {
|
||||
}
|
||||
|
||||
const enableAll = async () => {
|
||||
const promises = []
|
||||
for (const project of functionValues.value) {
|
||||
if (project.disabled) {
|
||||
await toggleDisableMod(project, false)
|
||||
promises.push(toggleDisableMod(project))
|
||||
}
|
||||
}
|
||||
await Promise.all(promises).catch(handleError)
|
||||
}
|
||||
|
||||
const disableAll = async () => {
|
||||
const promises = []
|
||||
for (const project of functionValues.value) {
|
||||
if (!project.disabled) {
|
||||
await toggleDisableMod(project, false)
|
||||
promises.push(toggleDisableMod(project))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hideShowAll = async () => {
|
||||
hideNonSelected.value = !hideNonSelected.value
|
||||
}
|
||||
|
||||
const handleRightClick = (event, mod) => {
|
||||
if (mod.slug && mod.project_type) {
|
||||
props.options.showMenu(
|
||||
event,
|
||||
{
|
||||
link: `https://modrinth.com/${mod.project_type}/${mod.slug}`,
|
||||
},
|
||||
[{ name: 'open_link' }, { name: 'copy_link' }],
|
||||
)
|
||||
}
|
||||
await Promise.all(promises).catch(handleError)
|
||||
}
|
||||
|
||||
watch(selectAll, () => {
|
||||
@@ -868,10 +763,6 @@ watch(selectAll, () => {
|
||||
}
|
||||
})
|
||||
|
||||
const switchPage = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
const refreshingProjects = ref(false)
|
||||
async function refreshProjects() {
|
||||
refreshingProjects.value = true
|
||||
@@ -1173,16 +1064,6 @@ onUnmounted(() => {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.updating-indicator {
|
||||
height: 2.25rem !important;
|
||||
width: 2.25rem !important;
|
||||
|
||||
svg {
|
||||
height: 1.25rem !important;
|
||||
width: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.select-checkbox {
|
||||
button.checkbox {
|
||||
border: none;
|
||||
@@ -1190,13 +1071,23 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-input {
|
||||
.selected {
|
||||
height: 2.5rem;
|
||||
}
|
||||
.search-input {
|
||||
min-height: 2.25rem;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.pagination-after {
|
||||
margin-bottom: 5rem;
|
||||
.top-box {
|
||||
background-image: radial-gradient(
|
||||
50% 100% at 50% 100%,
|
||||
var(--color-brand-highlight) 10%,
|
||||
#ffffff00 100%
|
||||
);
|
||||
}
|
||||
|
||||
.top-box-divider {
|
||||
background-image: linear-gradient(90deg, #ffffff00 0%, var(--color-brand) 50%, #ffffff00 100%);
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -897,7 +897,6 @@ async function saveGvLoaderEdits() {
|
||||
.change-versions-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
:deep(.animated-dropdown .options) {
|
||||
|
||||
Reference in New Issue
Block a user