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:
@@ -245,7 +245,7 @@ const colorVariables = computed(() => {
|
||||
}
|
||||
|
||||
&:not([disabled]):not([disabled='true']):not(.disabled) {
|
||||
@apply active:scale-95 hover:brightness-125 focus-visible:brightness-125 hover:bg-[--_hover-bg] hover:text-[--_hover-text] focus-visible:bg-[--_hover-bg] focus-visible:text-[--_hover-text];
|
||||
@apply active:scale-95 hover:brightness-[--hover-brightness] focus-visible:brightness-[--hover-brightness] hover:bg-[--_hover-bg] hover:text-[--_hover-text] focus-visible:bg-[--_hover-bg] focus-visible:text-[--_hover-text];
|
||||
|
||||
&:hover svg:first-child,
|
||||
&:focus-visible svg:first-child {
|
||||
|
||||
52
packages/ui/src/components/base/FilterBar.vue
Normal file
52
packages/ui/src/components/base/FilterBar.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div v-if="options.length > 1" class="flex flex-wrap gap-1 items-center">
|
||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||
<button
|
||||
v-for="filter in options"
|
||||
:key="`filter-${filter.id}`"
|
||||
: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="toggleFilter(filter.id)"
|
||||
>
|
||||
{{ formatMessage(filter.message) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FilterIcon } from '@modrinth/assets'
|
||||
import { watch } from 'vue'
|
||||
import { type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
export type FilterBarOption = {
|
||||
id: string
|
||||
message: MessageDescriptor
|
||||
}
|
||||
|
||||
const selectedFilters = defineModel<string[]>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
options: FilterBarOption[]
|
||||
}>()
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
() => {
|
||||
for (let i = 0; i < selectedFilters.value.length; i++) {
|
||||
const option = selectedFilters.value[i]
|
||||
if (!props.options.some((x) => x.id === option)) {
|
||||
selectedFilters.value.splice(i, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function toggleFilter(option: string) {
|
||||
if (selectedFilters.value.includes(option)) {
|
||||
selectedFilters.value.splice(selectedFilters.value.indexOf(option), 1)
|
||||
} else {
|
||||
selectedFilters.value.push(option)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
20
packages/ui/src/components/base/HeadingLink.vue
Normal file
20
packages/ui/src/components/base/HeadingLink.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<AutoLink
|
||||
:to="to"
|
||||
class="flex mb-3 leading-none items-center gap-1 text-primary text-lg font-bold hover:underline group w-fit"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRightIcon
|
||||
class="h-5 w-5 stroke-[3px] group-hover:translate-x-1 transition-transform group-hover:text-brand"
|
||||
/>
|
||||
</AutoLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AutoLink from './AutoLink.vue'
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
|
||||
defineProps<{
|
||||
to: unknown
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div :style="colorClasses" class="radial-header relative pb-1" v-bind="$attrs">
|
||||
<slot />
|
||||
<div>
|
||||
<div :style="colorClasses" class="radial-header relative" v-bind="$attrs">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="radial-header-divider" />
|
||||
</div>
|
||||
<div class="radial-header-divider" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
61
packages/ui/src/components/base/SmartClickable.vue
Normal file
61
packages/ui/src/components/base/SmartClickable.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="smart-clickable" :class="{ 'smart-clickable--has-clickable': !!$slots.clickable }">
|
||||
<slot name="clickable" />
|
||||
<div v-bind="$attrs" class="smart-clickable__contents pointer-events-none">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.smart-clickable {
|
||||
display: grid;
|
||||
|
||||
> * {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
.smart-clickable__contents {
|
||||
// Utility classes for contents
|
||||
:deep(.smart-clickable\:allow-pointer-events) {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only apply effects when a clickable is present
|
||||
.smart-clickable.smart-clickable--has-clickable {
|
||||
// Setup base styles for contents
|
||||
.smart-clickable__contents {
|
||||
transition: scale 0.125s ease-out;
|
||||
|
||||
// Why? I don't know. It forces the SVGs to render differently, which fixes some shift on hover otherwise.
|
||||
//filter: brightness(1.00001);
|
||||
}
|
||||
|
||||
// When clickable is being hovered or focus-visible, give contents an effect
|
||||
&:has(> *:first-child:hover, > *:first-child:focus-visible) .smart-clickable__contents {
|
||||
filter: var(--hover-filter-weak);
|
||||
|
||||
// Utility classes for contents
|
||||
:deep(.smart-clickable\:underline-on-hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
// Utility classes for contents
|
||||
:deep(.smart-clickable\:highlight-on-hover) {
|
||||
filter: brightness(var(--hover-brightness, 1.25));
|
||||
}
|
||||
}
|
||||
|
||||
// When clickable is being clicked, give contents an effect
|
||||
&:has(> *:first-child:active) .smart-clickable__contents {
|
||||
scale: 0.97;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,6 +17,9 @@ export { default as DropArea } from './base/DropArea.vue'
|
||||
export { default as DropdownSelect } from './base/DropdownSelect.vue'
|
||||
export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue'
|
||||
export { default as FileInput } from './base/FileInput.vue'
|
||||
export { default as FilterBar } from './base/FilterBar.vue'
|
||||
export type { FilterBarOption } from './base/FilterBar.vue'
|
||||
export { default as HeadingLink } from './base/HeadingLink.vue'
|
||||
export { default as LoadingIndicator } from './base/LoadingIndicator.vue'
|
||||
export { default as ManySelect } from './base/ManySelect.vue'
|
||||
export { default as MarkdownEditor } from './base/MarkdownEditor.vue'
|
||||
@@ -34,6 +37,7 @@ export { default as ScrollablePanel } from './base/ScrollablePanel.vue'
|
||||
export { default as ServerNotice } from './base/ServerNotice.vue'
|
||||
export { default as SimpleBadge } from './base/SimpleBadge.vue'
|
||||
export { default as Slider } from './base/Slider.vue'
|
||||
export { default as SmartClickable } from './base/SmartClickable.vue'
|
||||
export { default as StatItem } from './base/StatItem.vue'
|
||||
export { default as TagItem } from './base/TagItem.vue'
|
||||
export { default as TeleportDropdownMenu } from './base/TeleportDropdownMenu.vue'
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
</slot>
|
||||
</template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-if="description"
|
||||
class="markdown-body max-w-[35rem]"
|
||||
v-html="renderString(description)"
|
||||
/>
|
||||
<template v-if="description">
|
||||
<div
|
||||
v-if="markdown"
|
||||
class="markdown-body max-w-[35rem]"
|
||||
v-html="renderString(description)"
|
||||
/>
|
||||
<p v-else class="max-w-[35rem] m-0">
|
||||
{{ description }}
|
||||
</p>
|
||||
</template>
|
||||
<slot />
|
||||
<label v-if="hasToType" for="confirmation">
|
||||
<span>
|
||||
@@ -46,7 +51,7 @@
|
||||
|
||||
<script setup>
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import NewModal from './NewModal.vue'
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
@@ -92,6 +97,10 @@ const props = defineProps({
|
||||
return () => {}
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['proceed'])
|
||||
|
||||
@@ -20,6 +20,21 @@
|
||||
"button.edit": {
|
||||
"defaultMessage": "Edit"
|
||||
},
|
||||
"button.open-folder": {
|
||||
"defaultMessage": "Open folder"
|
||||
},
|
||||
"button.play": {
|
||||
"defaultMessage": "Play"
|
||||
},
|
||||
"button.refresh": {
|
||||
"defaultMessage": "Refresh"
|
||||
},
|
||||
"button.remove": {
|
||||
"defaultMessage": "Remove"
|
||||
},
|
||||
"button.remove-image": {
|
||||
"defaultMessage": "Remove image"
|
||||
},
|
||||
"button.report": {
|
||||
"defaultMessage": "Report"
|
||||
},
|
||||
@@ -35,6 +50,9 @@
|
||||
"button.sign-out": {
|
||||
"defaultMessage": "Sign out"
|
||||
},
|
||||
"button.stop": {
|
||||
"defaultMessage": "Stop"
|
||||
},
|
||||
"button.upload-image": {
|
||||
"defaultMessage": "Upload image"
|
||||
},
|
||||
@@ -65,6 +83,21 @@
|
||||
"input.view.list": {
|
||||
"defaultMessage": "Rows view"
|
||||
},
|
||||
"instance.worlds.game_mode.adventure": {
|
||||
"defaultMessage": "Adventure mode"
|
||||
},
|
||||
"instance.worlds.game_mode.creative": {
|
||||
"defaultMessage": "Creative mode"
|
||||
},
|
||||
"instance.worlds.game_mode.spectator": {
|
||||
"defaultMessage": "Spectator mode"
|
||||
},
|
||||
"instance.worlds.game_mode.survival": {
|
||||
"defaultMessage": "Survival mode"
|
||||
},
|
||||
"instance.worlds.game_mode.unknown": {
|
||||
"defaultMessage": "Unknown game mode"
|
||||
},
|
||||
"label.changes-saved": {
|
||||
"defaultMessage": "Changes saved"
|
||||
},
|
||||
@@ -89,6 +122,9 @@
|
||||
"label.followed-projects": {
|
||||
"defaultMessage": "Followed projects"
|
||||
},
|
||||
"label.loading": {
|
||||
"defaultMessage": "Loading..."
|
||||
},
|
||||
"label.moderation": {
|
||||
"defaultMessage": "Moderation"
|
||||
},
|
||||
@@ -98,6 +134,9 @@
|
||||
"label.password": {
|
||||
"defaultMessage": "Password"
|
||||
},
|
||||
"label.played": {
|
||||
"defaultMessage": "Played {time}"
|
||||
},
|
||||
"label.public": {
|
||||
"defaultMessage": "Public"
|
||||
},
|
||||
@@ -107,12 +146,18 @@
|
||||
"label.scopes": {
|
||||
"defaultMessage": "Scopes"
|
||||
},
|
||||
"label.server": {
|
||||
"defaultMessage": "Server"
|
||||
},
|
||||
"label.servers": {
|
||||
"defaultMessage": "Servers"
|
||||
},
|
||||
"label.settings": {
|
||||
"defaultMessage": "Settings"
|
||||
},
|
||||
"label.singleplayer": {
|
||||
"defaultMessage": "Singleplayer"
|
||||
},
|
||||
"label.title": {
|
||||
"defaultMessage": "Title"
|
||||
},
|
||||
|
||||
@@ -85,6 +85,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'input.view.list',
|
||||
defaultMessage: 'Rows view',
|
||||
},
|
||||
loadingLabel: {
|
||||
id: 'label.loading',
|
||||
defaultMessage: 'Loading...',
|
||||
},
|
||||
moderationLabel: {
|
||||
id: 'label.moderation',
|
||||
defaultMessage: 'Moderation',
|
||||
@@ -93,6 +97,14 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.notifications',
|
||||
defaultMessage: 'Notifications',
|
||||
},
|
||||
playButton: {
|
||||
id: 'button.play',
|
||||
defaultMessage: 'Play',
|
||||
},
|
||||
playedLabel: {
|
||||
id: 'label.played',
|
||||
defaultMessage: 'Played {time}',
|
||||
},
|
||||
privateLabel: {
|
||||
id: 'collection.label.private',
|
||||
defaultMessage: 'Private',
|
||||
@@ -101,14 +113,26 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.public',
|
||||
defaultMessage: 'Public',
|
||||
},
|
||||
refreshButton: {
|
||||
id: 'button.refresh',
|
||||
defaultMessage: 'Refresh',
|
||||
},
|
||||
rejectedLabel: {
|
||||
id: 'label.rejected',
|
||||
defaultMessage: 'Rejected',
|
||||
},
|
||||
removeButton: {
|
||||
id: 'button.remove',
|
||||
defaultMessage: 'Remove',
|
||||
},
|
||||
reportButton: {
|
||||
id: 'button.report',
|
||||
defaultMessage: 'Report',
|
||||
},
|
||||
openFolderButton: {
|
||||
id: 'button.open-folder',
|
||||
defaultMessage: 'Open folder',
|
||||
},
|
||||
passwordLabel: {
|
||||
id: 'label.password',
|
||||
defaultMessage: 'Password',
|
||||
@@ -125,6 +149,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.scopes',
|
||||
defaultMessage: 'Scopes',
|
||||
},
|
||||
serverLabel: {
|
||||
id: 'label.server',
|
||||
defaultMessage: 'Server',
|
||||
},
|
||||
serversLabel: {
|
||||
id: 'label.servers',
|
||||
defaultMessage: 'Servers',
|
||||
@@ -141,6 +169,14 @@ export const commonMessages = defineMessages({
|
||||
id: 'button.sign-out',
|
||||
defaultMessage: 'Sign out',
|
||||
},
|
||||
singleplayerLabel: {
|
||||
id: 'label.singleplayer',
|
||||
defaultMessage: 'Singleplayer',
|
||||
},
|
||||
stopButton: {
|
||||
id: 'button.stop',
|
||||
defaultMessage: 'Stop',
|
||||
},
|
||||
titleLabel: {
|
||||
id: 'label.title',
|
||||
defaultMessage: 'Title',
|
||||
|
||||
40
packages/ui/src/utils/game-modes.ts
Normal file
40
packages/ui/src/utils/game-modes.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BlocksIcon, CompassIcon, EyeIcon, PickaxeIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import { defineMessage } from '@vintl/vintl'
|
||||
|
||||
export const GAME_MODES = {
|
||||
survival: {
|
||||
icon: PickaxeIcon,
|
||||
message: defineMessage({
|
||||
id: 'instance.worlds.game_mode.survival',
|
||||
defaultMessage: 'Survival mode',
|
||||
}),
|
||||
},
|
||||
creative: {
|
||||
icon: BlocksIcon,
|
||||
message: defineMessage({
|
||||
id: 'instance.worlds.game_mode.creative',
|
||||
defaultMessage: 'Creative mode',
|
||||
}),
|
||||
},
|
||||
adventure: {
|
||||
icon: CompassIcon,
|
||||
message: defineMessage({
|
||||
id: 'instance.worlds.game_mode.adventure',
|
||||
defaultMessage: 'Adventure mode',
|
||||
}),
|
||||
},
|
||||
spectator: {
|
||||
icon: EyeIcon,
|
||||
message: defineMessage({
|
||||
id: 'instance.worlds.game_mode.spectator',
|
||||
defaultMessage: 'Spectator mode',
|
||||
}),
|
||||
},
|
||||
unknown: {
|
||||
icon: UnknownIcon,
|
||||
message: defineMessage({
|
||||
id: 'instance.worlds.game_mode.unknown',
|
||||
defaultMessage: 'Unknown game mode',
|
||||
}),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user