mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
menu refactor + web account switcher (#7307)
* add account switcher, migrate context menus to overflow menu system, update version filter controls, only pad instance icons * app account switcher, improve some context menus, theme stuff * prepr * handle reauthentication * fix a couple sign in issues, admin page cleanup, fix org status badges, fix official account badge, open in local keybind, publish plus icons * rename generic menus to ButtonMenu (& types) * fix hydration issue w/ dropdowns + middleware error
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
CompassIcon,
|
||||
ExternalIcon,
|
||||
GlobeIcon,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
BrowsePageLayout,
|
||||
BrowseSidebar,
|
||||
commonMessages,
|
||||
ContextMenu,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
formatProjectTypeSentence,
|
||||
@@ -38,7 +38,6 @@ import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { useAppSettings } from '@/composables/use-app-settings.ts'
|
||||
@@ -516,7 +515,6 @@ const {
|
||||
getServerModpackContent,
|
||||
getServerCardActions,
|
||||
handleRightClick,
|
||||
handleOptionsClick,
|
||||
} = useAppServerBrowse({
|
||||
instance,
|
||||
isFromWorlds,
|
||||
@@ -551,6 +549,10 @@ const messages = defineMessages({
|
||||
id: 'app.browse.add-servers-to-instance',
|
||||
defaultMessage: 'Adding server to instance',
|
||||
},
|
||||
projectActionsLabel: {
|
||||
id: 'app.browse.project-actions.label',
|
||||
defaultMessage: 'Project actions',
|
||||
},
|
||||
addToAnInstance: {
|
||||
id: 'app.browse.add-to-an-instance',
|
||||
defaultMessage: 'Add to an instance',
|
||||
@@ -1331,12 +1333,9 @@ provideBrowseManager({
|
||||
<div class="flex flex-col gap-3 p-6">
|
||||
<BrowsePageLayout>
|
||||
<template #after>
|
||||
<ContextMenu ref="contextMenuRef" @option-clicked="handleOptionsClick">
|
||||
<template #open_link>
|
||||
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
|
||||
</template>
|
||||
<template #copy_link>
|
||||
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
|
||||
<ContextMenu ref="contextMenuRef" :label="formatMessage(messages.projectActionsLabel)">
|
||||
<template #open_link="{ option }">
|
||||
<GlobeIcon /> {{ option.label }} <ExternalIcon />
|
||||
</template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { PlayIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { ContextMenu, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, inject, onActivated, ref } from 'vue'
|
||||
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import LibrarySection from '@/components/ui/library/index.vue'
|
||||
import WelcomeScreen from '@/components/ui/WelcomeScreen.vue'
|
||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||
@@ -36,6 +35,10 @@ const messages = defineMessages({
|
||||
id: 'app.library.context-menu.create-instance',
|
||||
defaultMessage: 'New instance',
|
||||
},
|
||||
libraryActionsLabel: {
|
||||
id: 'app.library.actions.label',
|
||||
defaultMessage: 'Library actions',
|
||||
},
|
||||
})
|
||||
|
||||
const homeBreadcrumb = useRootBreadcrumb({
|
||||
@@ -87,13 +90,14 @@ function openPageContextMenu(event: MouseEvent) {
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
pageOptions.value?.showMenu(event, {}, [{ name: 'new_instance' }])
|
||||
}
|
||||
|
||||
function handlePageOption({ option }: { option: string }) {
|
||||
if (option === 'new_instance') {
|
||||
showCreationModal?.()
|
||||
}
|
||||
pageOptions.value?.open(event, [
|
||||
{
|
||||
id: 'new_instance',
|
||||
label: formatMessage(messages.newInstance),
|
||||
icon: PlusIcon,
|
||||
action: () => showCreationModal?.(),
|
||||
},
|
||||
])
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -110,8 +114,6 @@ function handlePageOption({ option }: { option: string }) {
|
||||
:recent-instances="recentInstances"
|
||||
/>
|
||||
<LibrarySection :instances="instances" />
|
||||
<ContextMenu ref="pageOptions" @option-clicked="handlePageOption">
|
||||
<template #new_instance> <PlusIcon /> {{ formatMessage(messages.newInstance) }} </template>
|
||||
</ContextMenu>
|
||||
<ContextMenu ref="pageOptions" :label="formatMessage(messages.libraryActionsLabel)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<PageHeader :title="instance.name">
|
||||
<template #leading>
|
||||
<Avatar :src="iconSrc" :alt="instance.name" size="64px" :tint-by="instance.id" />
|
||||
<Avatar
|
||||
:src="iconSrc"
|
||||
:alt="instance.name"
|
||||
size="64px"
|
||||
:tint-by="instance.id"
|
||||
pad-transparent-corners
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="instance.shared_instance || instance.quarantined" #badges>
|
||||
@@ -181,10 +187,10 @@ import {
|
||||
import { Button, IconButton, SplitButton, TeleportOverflowMenu } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
type ButtonMenuOption,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
formatLoaderLabel,
|
||||
type OverflowMenuOption,
|
||||
PageHeader,
|
||||
PageHeaderActions,
|
||||
PageHeaderBadgeItem,
|
||||
@@ -344,7 +350,7 @@ const playtimeLabel = computed(() => {
|
||||
|
||||
return `${seconds} second${seconds === 1 ? '' : 's'}`
|
||||
})
|
||||
const serverPlayOptions = computed<OverflowMenuOption[]>(() => [
|
||||
const serverPlayOptions = computed<ButtonMenuOption[]>(() => [
|
||||
{
|
||||
id: 'launch_instance',
|
||||
label: formatMessage(messages.launchInstance),
|
||||
@@ -352,8 +358,8 @@ const serverPlayOptions = computed<OverflowMenuOption[]>(() => [
|
||||
action: () => emit('play'),
|
||||
},
|
||||
])
|
||||
const moreActions = computed<OverflowMenuOption[]>(() => {
|
||||
const actions: OverflowMenuOption[] = [
|
||||
const moreActions = computed<ButtonMenuOption[]>(() => {
|
||||
const actions: ButtonMenuOption[] = [
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: formatMessage(messages.openFolder),
|
||||
|
||||
@@ -344,6 +344,7 @@ const messages = defineMessages({
|
||||
class="transition-[filter] group-hover:brightness-75"
|
||||
:tint-by="instance.id"
|
||||
no-shadow
|
||||
pad-transparent-corners
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 h-full w-full flex items-center justify-center opacity-0 transition-all group-hover:opacity-100"
|
||||
|
||||
@@ -200,6 +200,7 @@ defineExpose({ show, hide })
|
||||
:src="getInstanceIconUrl(instance.icon_path)"
|
||||
size="24px"
|
||||
:tint-by="props.instance.id"
|
||||
pad-transparent-corners
|
||||
/>
|
||||
{{ instance.name }} <ChevronRightIcon />
|
||||
<span class="font-extrabold text-contrast">{{
|
||||
|
||||
@@ -92,6 +92,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ClipboardCopyIcon, FolderOpenIcon, LockIcon, LockOpenIcon } from '@modrinth/assets'
|
||||
import {
|
||||
type BulkOperationStatus,
|
||||
type ButtonMenuOption,
|
||||
commonMessages,
|
||||
ConfirmDisableModal,
|
||||
ConfirmModpackUpdateModal,
|
||||
@@ -107,7 +108,6 @@ import {
|
||||
type ManagedContentModalState,
|
||||
type ManagedContentProject,
|
||||
type ManagedContentVersion,
|
||||
type OverflowMenuOption,
|
||||
provideContentManager,
|
||||
ReadyTransition,
|
||||
summarizeManagedContent,
|
||||
@@ -1473,8 +1473,8 @@ async function handleShareItems(
|
||||
await shareModal.value?.show(text)
|
||||
}
|
||||
|
||||
function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
||||
const options: OverflowMenuOption[] = []
|
||||
function getOverflowOptions(item: ContentItem): ButtonMenuOption[] {
|
||||
const options: ButtonMenuOption[] = []
|
||||
|
||||
options.push({
|
||||
id: 'show-file',
|
||||
|
||||
@@ -82,16 +82,7 @@
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> {{ formatMessage(messages.play) }} </template>
|
||||
<template #stop> <StopCircleIcon /> {{ formatMessage(messages.stop) }} </template>
|
||||
<template #add_content> <PlusIcon /> {{ formatMessage(messages.addContent) }} </template>
|
||||
<template #edit> <EditIcon /> {{ formatMessage(messages.edit) }} </template>
|
||||
<template #copy_path> <ClipboardCopyIcon /> {{ formatMessage(messages.copyPath) }} </template>
|
||||
<template #open_folder>
|
||||
<FolderOpenIcon /> {{ formatMessage(messages.openFolder) }}
|
||||
</template>
|
||||
</ContextMenu>
|
||||
<ContextMenu ref="options" :label="formatMessage(messages.instanceActionsLabel)" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -109,6 +100,7 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
commonMessages,
|
||||
ContextMenu,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
@@ -122,7 +114,6 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, type ComputedRef, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||
@@ -190,6 +181,10 @@ const messages = defineMessages({
|
||||
edit: { id: 'app.instance.action.edit', defaultMessage: 'Edit' },
|
||||
copyPath: { id: 'app.instance.action.copy-path', defaultMessage: 'Copy path' },
|
||||
openFolder: { id: 'app.instance.action.open-folder', defaultMessage: 'Open folder' },
|
||||
instanceActionsLabel: {
|
||||
id: 'app.instance.actions.label',
|
||||
defaultMessage: 'Instance actions',
|
||||
},
|
||||
contentTab: { id: 'app.instance.tab.content', defaultMessage: 'Content' },
|
||||
filesTab: { id: 'app.instance.tab.files', defaultMessage: 'Files' },
|
||||
worldsTab: { id: 'app.instance.tab.worlds', defaultMessage: 'Worlds' },
|
||||
@@ -765,63 +760,60 @@ async function deleteSelectedInstance() {
|
||||
}
|
||||
|
||||
const handleRightClick = (event: MouseEvent) => {
|
||||
const baseOptions = [
|
||||
...(instance.value?.quarantined ? [] : [{ name: 'add_content' }, { type: 'divider' }]),
|
||||
{ name: 'edit' },
|
||||
{ name: 'open_folder' },
|
||||
{ name: 'copy_path' },
|
||||
]
|
||||
const canAddContent = !instance.value?.quarantined
|
||||
|
||||
options.value?.showMenu(
|
||||
event,
|
||||
instance.value,
|
||||
playing.value
|
||||
? [
|
||||
{
|
||||
name: 'stop',
|
||||
color: 'danger',
|
||||
},
|
||||
...baseOptions,
|
||||
]
|
||||
: [
|
||||
...(instance.value?.quarantined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'play',
|
||||
color: 'primary',
|
||||
},
|
||||
]),
|
||||
...baseOptions,
|
||||
],
|
||||
)
|
||||
options.value?.open(event, [
|
||||
{
|
||||
id: 'stop',
|
||||
label: formatMessage(messages.stop),
|
||||
icon: StopCircleIcon,
|
||||
shown: playing.value,
|
||||
tone: 'red',
|
||||
action: () => void stopInstance('InstancePageContextMenu'),
|
||||
},
|
||||
{
|
||||
id: 'play',
|
||||
label: formatMessage(messages.play),
|
||||
icon: PlayIcon,
|
||||
shown: !playing.value && canAddContent,
|
||||
tone: 'brand',
|
||||
action: () => void startInstance('InstancePageContextMenu'),
|
||||
},
|
||||
{
|
||||
id: 'add_content',
|
||||
label: formatMessage(messages.addContent),
|
||||
icon: PlusIcon,
|
||||
shown: canAddContent,
|
||||
action: () => void browseContent(instance.value?.loader === 'vanilla' ? 'datapack' : 'mod'),
|
||||
},
|
||||
{ type: 'divider', shown: canAddContent },
|
||||
{
|
||||
id: 'edit',
|
||||
label: formatMessage(messages.edit),
|
||||
icon: EditIcon,
|
||||
action: openSettings,
|
||||
},
|
||||
{
|
||||
id: 'open_folder',
|
||||
label: formatMessage(messages.openFolder),
|
||||
icon: FolderOpenIcon,
|
||||
action: () => {
|
||||
if (instance.value) void showInstanceInFolder(instance.value.id)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'copy_path',
|
||||
label: formatMessage(messages.copyPath),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => void copyInstancePath(),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
||||
switch (args.option) {
|
||||
case 'play':
|
||||
await startInstance('InstancePageContextMenu')
|
||||
break
|
||||
case 'stop':
|
||||
await stopInstance('InstancePageContextMenu')
|
||||
break
|
||||
case 'add_content':
|
||||
await browseContent(instance.value?.loader === 'vanilla' ? 'datapack' : 'mod')
|
||||
break
|
||||
case 'edit':
|
||||
openSettings()
|
||||
break
|
||||
case 'open_folder':
|
||||
if (instance.value) await showInstanceInFolder(instance.value.id)
|
||||
break
|
||||
case 'copy_path': {
|
||||
if (instance.value) {
|
||||
const fullPath = await get_full_path(instance.value.id)
|
||||
await navigator.clipboard.writeText(fullPath)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
const copyInstancePath = async () => {
|
||||
if (!instance.value) return
|
||||
const fullPath = await get_full_path(instance.value.id)
|
||||
await navigator.clipboard.writeText(fullPath)
|
||||
}
|
||||
|
||||
provideInstancePage({
|
||||
|
||||
@@ -208,15 +208,9 @@
|
||||
v-if="projectInstallContext"
|
||||
:install-context="projectInstallContext"
|
||||
/>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install>
|
||||
<DownloadIcon /> {{ formatMessage(commonMessages.installButton) }}
|
||||
</template>
|
||||
<template #open_link>
|
||||
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
|
||||
</template>
|
||||
<template #copy_link>
|
||||
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
|
||||
<ContextMenu ref="options" :label="formatMessage(messages.projectActionsLabel)">
|
||||
<template #open_link="{ option }">
|
||||
<GlobeIcon /> {{ option.label }} <ExternalIcon />
|
||||
</template>
|
||||
</ContextMenu>
|
||||
<CreationFlowModal
|
||||
@@ -260,6 +254,7 @@ import {
|
||||
BrowseInstallHeader,
|
||||
Button,
|
||||
commonMessages,
|
||||
ContextMenu,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
getTargetInstallPreferences,
|
||||
@@ -287,7 +282,6 @@ import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import ContextMenu from '@/components/ui/context-menu/index.vue'
|
||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||
import {
|
||||
fetchCachedServerStatus,
|
||||
@@ -353,6 +347,7 @@ const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
moreOptions: { id: 'app.project.more-options', defaultMessage: 'More options' },
|
||||
projectActionsLabel: { id: 'app.project.actions.label', defaultMessage: 'Project actions' },
|
||||
descriptionTab: { id: 'app.project.tab.description', defaultMessage: 'Description' },
|
||||
versionsTab: { id: 'app.project.tab.versions', defaultMessage: 'Versions' },
|
||||
galleryTab: { id: 'app.project.tab.gallery', defaultMessage: 'Gallery' },
|
||||
@@ -926,36 +921,32 @@ async function install(version) {
|
||||
|
||||
const options = ref(null)
|
||||
const handleRightClick = (event) => {
|
||||
options.value.showMenu(event, data.value, [
|
||||
const project = data.value
|
||||
options.value.open(event, [
|
||||
{
|
||||
name: 'install',
|
||||
id: 'install',
|
||||
label: formatMessage(commonMessages.installButton),
|
||||
icon: DownloadIcon,
|
||||
action: () => install(null),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
id: 'open_link',
|
||||
label: formatMessage(commonMessages.openInModrinthButton),
|
||||
icon: GlobeIcon,
|
||||
action: () => openProjectLink(project),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
id: 'copy_link',
|
||||
label: formatMessage(commonMessages.copyLinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => copyProjectLink(project),
|
||||
},
|
||||
])
|
||||
}
|
||||
const handleOptionsClick = (args) => {
|
||||
switch (args.option) {
|
||||
case 'install':
|
||||
install(null)
|
||||
break
|
||||
case 'open_link':
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
const getProjectLink = (project) => `https://modrinth.com/${project.project_type}/${project.slug}`
|
||||
const openProjectLink = (project) => openUrl(getProjectLink(project))
|
||||
const copyProjectLink = (project) => navigator.clipboard.writeText(getProjectLink(project))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user