You've already forked AstralRinth
forked from didirus/AstralRinth
Direct World Joining (#3457)
* Begin work on worlds backend * Finish implementing get_profile_worlds and get_server_status (except pinning) * Create TS types and manually copy unparsed chat components * Clippy fix * Update types.d.ts * Initial worlds UI work * Fix api::get_profile_worlds to take in a relative path * sanitize & security update * Fix sanitizePotentialFileUrl * Fix sanitizePotentialFileUrl (for real) * Fix empty motd causing error * Finally actually fix world icons * Fix world icon not being visible on non-Windows * Use the correct generics to take in AppHandle * Implement start_join_singleplayer_world and start_join_server for modern versions * Don't error if server has no cached icon * Migrate to own server pinging * Ignore missing server hidden field and missing saves dir * Update world list frontend * More frontend work * Server status player sample can be absent * Fix refresh state * Add get_profile_protocol_version * Add protocol_version column to database * SQL INTEGER is i64 in sqlx * sqlx prepare * Cache protocol version in database * Continue worlds UI work * Fix motds being bold * Remove legacy pinging and add a 30-second timeout * Remove pinned for now and match world (and server) parsing closer to spec * Move type ServerStatus to worlds.ts * Implement add_server_to_profile * Fix pack_status being ignored when joining from launcher * Make World path field be relative * Implement rename_world and reset_world_icon * Clippy fix * Fix rename_world * UI enhancements * Implement backup_world, which returns the backup size in bytes * Clippy fix * Return index when adding servers to profile * Fix backup * Implement delete_world * Implement edit_server_in_profile and remove_server_from_profile * Clippy fix * Log server joins * Add edit and delete support * Fix ts errors * Fix minecraft font * Switch font out for non-monospaced. * Fix font proper * Some more world cleanup, handle play state, check quickplay compatibility * Clear the cached protocol version when a profile's game version is changed * Fix tint colors in navbar * Fix server protocol version pinging * UI fixes * Fix protocol version handler * Fix MOTD parsing * Add worlds_updated profile event * fix pkg * Functional home screen with worlds * lint * Fix incorrect folder creation * Make items clickable * Add locked field to SingleplayerWorld indicating whether the world is locked by the game * Implement locking frontend * Fix locking condition * Split worlds_updated profile event into servers_updated and world_updated * Fix compile error * Use port from resolve SRV record * Fix serialization of ProfilePayload and ProfilePayloadType * Individual singleplayer world refreshing * Log when worlds are perceived to be updated * Push logging + total refresh lock * Unlisten fixes * Highlight current world when clicked * Launcher logs refactor (#3444) * Switch live log to use STDOUT * fix clippy, legacy logs support * Fix lint * Handle non-XML log messages in XML logging, and don't escape log messages into XML --------- Co-authored-by: Josiah Glosson <soujournme@gmail.com> * Update incompatibility text * Home page fixes, and unlock after close * Remove logging * Add join log database migration * Switch server join timing to being in the database instead of in a separate log file * Create optimized get_recent_worlds function that takes in a limit * Update dependencies and fix Cargo.lock * temp disable overflow menus * revert home page changes * Enable overflow menus again * Remove list * Revert * Push dev tools * Remove default filter * Disable debug renderer * Fix random app errors * Refactor * Fix missing computed import * Fix light mode issues * Fix TS errors * Lint * Fix bad link in change modpack version modal * fix lint * fix intl --------- Co-authored-by: Josiah Glosson <soujournme@gmail.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
@@ -1,251 +1,252 @@
|
||||
<template>
|
||||
<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 ${filteredProjects.length} project${filteredProjects.length === 1 ? '' : 's'}...`"
|
||||
class="text-input search-input"
|
||||
autocomplete="off"
|
||||
<div>
|
||||
<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 ${filteredProjects.length} project${filteredProjects.length === 1 ? '' : 's'}...`"
|
||||
class="text-input search-input"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (searchFilter = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<AddContentButton :instance="instance" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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)"
|
||||
>
|
||||
{{ filter.formattedName }}
|
||||
</button>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="search.length > 0"
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(search.length / 20)"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="(page) => (currentPage = page)"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (searchFilter = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<AddContentButton :instance="instance" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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)"
|
||||
>
|
||||
{{ filter.formattedName }}
|
||||
</button>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="search.length > 0"
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(search.length / 20)"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="(page) => (currentPage = page)"
|
||||
/>
|
||||
</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: {},
|
||||
<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.author) {
|
||||
item.creator = {
|
||||
name: x.author.name,
|
||||
type: x.author.type,
|
||||
id: x.author.slug,
|
||||
link: `https://modrinth.com/${x.author.type}/${x.author.slug}`,
|
||||
linkProps: { target: '_blank' },
|
||||
if (x.version) {
|
||||
item.version = x.version
|
||||
item.versionId = x.version
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
"
|
||||
:sort-column="sortColumn"
|
||||
:sort-ascending="ascending"
|
||||
:update-sort="sortProjects"
|
||||
:current-page="currentPage"
|
||||
>
|
||||
<template v-if="selectedProjects.length > 0" #headers>
|
||||
<div class="flex gap-2">
|
||||
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.name,
|
||||
type: x.author.type,
|
||||
id: x.author.slug,
|
||||
link: `https://modrinth.com/${x.author.type}/${x.author.slug}`,
|
||||
linkProps: { target: '_blank' },
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
"
|
||||
:sort-column="sortColumn"
|
||||
:sort-ascending="ascending"
|
||||
:update-sort="sortProjects"
|
||||
:current-page="currentPage"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<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>
|
||||
</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 && selectedProjects.some((m) => m.outdated)"
|
||||
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 @click="updateSelected()"><DownloadIcon /> Update</button>
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal.show()">
|
||||
<DownloadIcon /> Update pack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<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)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-else class="w-[36px]"></div>
|
||||
<Toggle
|
||||
class="!mx-2"
|
||||
:model-value="!item.data.disabled"
|
||||
@update:model-value="toggleDisableMod(item.data)"
|
||||
/>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button v-tooltip="'Remove'" @click="removeMod(item)">
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'share-names',
|
||||
action: () => shareNames(),
|
||||
id: 'show-file',
|
||||
action: () => highlightModInProfile(instance.path, item.path),
|
||||
},
|
||||
{
|
||||
id: 'share-file-names',
|
||||
action: () => shareFileNames(),
|
||||
},
|
||||
{
|
||||
id: 'share-urls',
|
||||
action: () => shareUrls(),
|
||||
},
|
||||
{
|
||||
id: 'share-markdown',
|
||||
action: () => shareMarkdown(),
|
||||
id: 'copy-link',
|
||||
shown: item.data !== undefined && item.data.slug !== undefined,
|
||||
action: () => copyModLink(item),
|
||||
},
|
||||
]"
|
||||
direction="left"
|
||||
>
|
||||
<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>
|
||||
<MoreVerticalIcon />
|
||||
<template #show-file> <ExternalIcon /> Show file </template>
|
||||
<template #copy-link> <ClipboardCopyIcon /> Copy link </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>
|
||||
</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)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-else class="w-[36px]"></div>
|
||||
<Toggle
|
||||
class="!mx-2"
|
||||
:model-value="!item.data.disabled"
|
||||
@update:model-value="toggleDisableMod(item.data)"
|
||||
</template>
|
||||
</ContentListPanel>
|
||||
<div class="flex justify-end mt-4">
|
||||
<Pagination
|
||||
v-if="search.length > 0"
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(search.length / 20)"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="(page) => (currentPage = page)"
|
||||
/>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button v-tooltip="'Remove'" @click="removeMod(item)">
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'show-file',
|
||||
action: () => highlightModInProfile(instance.path, item.path),
|
||||
},
|
||||
{
|
||||
id: 'copy-link',
|
||||
shown: item.data !== undefined && item.data.slug !== undefined,
|
||||
action: () => copyModLink(item),
|
||||
},
|
||||
]"
|
||||
direction="left"
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="w-full max-w-[48rem] mx-auto flex flex-col mt-6">
|
||||
<RadialHeader class="">
|
||||
<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
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #show-file> <ExternalIcon /> Show file </template>
|
||||
<template #copy-link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ContentListPanel>
|
||||
<div class="flex justify-end mt-4">
|
||||
<Pagination
|
||||
v-if="search.length > 0"
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(search.length / 20)"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="(page) => (currentPage = page)"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</RadialHeader>
|
||||
<div class="flex mt-4 mx-auto">
|
||||
<AddContentButton :instance="instance" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-box-divider"></div>
|
||||
<div class="flex items-center gap-6 py-4">
|
||||
<AddContentButton :instance="instance" />
|
||||
</div>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
share-title="Sharing modpack content"
|
||||
share-text="Check out the projects I'm using in my modpack!"
|
||||
:open-in-new-tab="false"
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ModpackVersionModal
|
||||
v-if="instance.linked_data"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="props.versions"
|
||||
/>
|
||||
</div>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
share-title="Sharing modpack content"
|
||||
share-text="Check out the projects I'm using in my modpack!"
|
||||
:open-in-new-tab="false"
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ModpackVersionModal
|
||||
v-if="instance.linked_data"
|
||||
ref="modpackVersionModal"
|
||||
:instance="instance"
|
||||
:versions="props.versions"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
@@ -272,6 +273,7 @@ import {
|
||||
ContentListPanel,
|
||||
OverflowMenu,
|
||||
Pagination,
|
||||
RadialHeader,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
@@ -323,10 +325,22 @@ const props = defineProps({
|
||||
return false
|
||||
},
|
||||
},
|
||||
playing: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type ProjectListEntryAuthor = {
|
||||
@@ -458,7 +472,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
})
|
||||
}
|
||||
|
||||
projects.value = newProjects
|
||||
projects.value = newProjects ?? []
|
||||
|
||||
const newSelectionMap = new Map()
|
||||
for (const project of projects.value) {
|
||||
|
||||
Reference in New Issue
Block a user