add keybinds to copy/open prod links (#7294)

This commit is contained in:
Prospector
2026-08-24 16:29:36 -07:00
committed by GitHub
parent 4b5edb9a99
commit ad83117ebf
7 changed files with 122 additions and 16 deletions
@@ -1,10 +1,14 @@
<template>
<div>
<span class="flex flex-row items-center gap-2 text-sm text-secondary">
<GlobeIcon
<BoxIcon
v-if="props.scope === 'project'"
v-tooltip="'Can be used without the checklist open if setting enabled.'"
/>
<GlobeIcon
v-if="props.scope === 'global'"
v-tooltip="'Can be used anywhere on the website.'"
/>
<ShieldCheckIcon
v-if="props.scope === 'tech-review'"
v-tooltip="'Used within the tech review pages'"
@@ -49,7 +53,7 @@
</template>
<script setup lang="ts">
import { GlobeIcon, RotateCounterClockwiseIcon, ShieldCheckIcon } from '@modrinth/assets'
import { BoxIcon, GlobeIcon, RotateCounterClockwiseIcon, ShieldCheckIcon } from '@modrinth/assets'
import { type KeybindDefinition, toKeybindDefinition } from '@modrinth/moderation'
import { IconButton } from '@modrinth/ui'
import { onUnmounted } from 'vue'
@@ -2,6 +2,7 @@ import { type KeybindDefinition, Keybinds, Settings } from '@modrinth/moderation
import { computed } from 'vue'
import type { CookieOptions } from '#app'
import { FrontendNotificationManager } from '~/providers/frontend-notifications'
const moderationKeybindsId = 'moderation-keybinds'
const moderationSettingsId = 'moderation-settings'
@@ -83,3 +84,15 @@ export const saveModerationOptions = () => {
settings: options.value.settings,
}
}
let copyNotificationManager: FrontendNotificationManager | undefined
export function notifyCopied(value: string, title: string) {
copyNotificationManager ??= new FrontendNotificationManager()
copyNotificationManager.addNotification({
type: 'success',
title,
text: value,
autoCloseMs: 1000,
})
}
@@ -633,6 +633,7 @@ import ProjectDownloadModal from '~/components/ui/ProjectDownloadModal/index.vue
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
import { getSignInRouteObj } from '~/composables/auth.ts'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
import { notifyCopied } from '~/composables/moderation.ts'
import { STALE_TIME, STALE_TIME_LONG, warmProjectCheckCaches } from '~/composables/queries/project'
import { versionQueryOptions } from '~/composables/queries/version'
import { useServerInstallContent } from '~/composables/use-server-install-content'
@@ -2375,6 +2376,7 @@ function handleKeybinds(event) {
keybinds.value.handle(event, {
project: projectRaw.value,
scope: 'project',
notifyCopied,
})
}
@@ -0,0 +1,25 @@
import { notifyCopied, useModerationKeybinds } from '~/composables/moderation.ts'
function getOfficialOrigin(apiBaseUrl: string): string {
if (apiBaseUrl.startsWith('https://staging-api.modrinth.com')) {
return 'https://staging.modrinth.com'
}
return 'https://modrinth.com'
}
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const keybinds = useModerationKeybinds()
window.addEventListener('keydown', (event) => {
if (event.repeat) {
return
}
keybinds.value.handle(event, {
scope: 'global',
officialUrl: `${getOfficialOrigin(String(config.public.apiBaseUrl))}${window.location.pathname}${window.location.search}`,
notifyCopied,
})
})
})
+53 -5
View File
@@ -26,6 +26,17 @@ const copyProjectLink = async (
}
await navigator.clipboard.writeText(url)
return url
}
function isOfficialModrinthHost(): boolean {
const host = globalThis.location?.hostname
return host === 'modrinth.com' || host === 'www.modrinth.com' || host === 'staging.modrinth.com'
}
function isLocalhost(): boolean {
const host = globalThis.location?.hostname
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]'
}
const keybinds: { [id: string]: KeybindListener } = {
@@ -71,31 +82,68 @@ const keybinds: { [id: string]: KeybindListener } = {
keybind: 'Ctrl+Alt+C',
description: 'Copy permalink',
scope: 'project',
action: async (ctx) => copyProjectLink(ctx.project, true, false, false),
action: async (ctx) => {
const url = await copyProjectLink(ctx.project, true, false, false)
ctx.notifyCopied(url, 'Copied permalink to clipboard')
},
},
'copy-relative-permalink': {
keybind: 'Ctrl+Alt+R',
description: 'Copy relative permalink',
scope: 'project',
action: async (ctx) => copyProjectLink(ctx.project, true, true, false),
action: async (ctx) => {
const url = await copyProjectLink(ctx.project, true, true, false)
ctx.notifyCopied(url, 'Copied relative permalink to clipboard')
},
},
'copy-page-permalink': {
keybind: 'Shift+Ctrl+Alt+C',
description: 'Copy permalink with page',
scope: 'project',
action: async (ctx) => copyProjectLink(ctx.project, true, false, true),
action: async (ctx) => {
const url = await copyProjectLink(ctx.project, true, false, true)
ctx.notifyCopied(url, 'Copied permalink with page to clipboard')
},
},
'copy-page-relative-permalink': {
keybind: 'Shift+Ctrl+Alt+R',
description: 'Copy relative permalink with page',
scope: 'project',
action: async (ctx) => copyProjectLink(ctx.project, true, true, true),
action: async (ctx) => {
const url = await copyProjectLink(ctx.project, true, true, true)
ctx.notifyCopied(url, 'Copied relative permalink with page to clipboard')
},
},
'copy-id': {
keybind: 'Ctrl+Alt+D',
description: 'Copy Project ID',
scope: 'project',
action: async (ctx) => await navigator.clipboard.writeText(ctx.project.id),
action: async (ctx) => {
await navigator.clipboard.writeText(ctx.project.id)
ctx.notifyCopied(ctx.project.id, 'Copied Project ID to clipboard')
},
},
'open-official-site': {
keybind: 'Ctrl+Shift+P',
description: 'Open current page on production/staging',
scope: 'global',
enabled: () => !isOfficialModrinthHost(),
action: (ctx) => {
globalThis.open(ctx.officialUrl, '_blank', 'noopener,noreferrer')
},
},
'copy-official-site': {
keybind: 'Ctrl+Shift+O',
description: 'Copy production/staging URL (localhost only)',
scope: 'global',
enabled: () => isLocalhost(),
action: async (ctx) => {
await navigator.clipboard.writeText(ctx.officialUrl)
const environment = ctx.officialUrl.startsWith('https://staging.modrinth.com')
? 'staging'
: 'production'
ctx.notifyCopied(ctx.officialUrl, `Copied ${environment} URL to clipboard`)
},
},
'approve-project': {
keybind: 'Shift+Alt+A',
+6 -5
View File
@@ -44,11 +44,12 @@ export class Keybinds {
handle(event: KeyboardEvent, ctx: ModerationContext): boolean {
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target as HTMLElement)?.closest('.cm-editor') ||
(event.target as HTMLElement)?.classList?.contains('cm-content') ||
(event.target as HTMLElement)?.classList?.contains('cm-line')
ctx.scope !== 'global' &&
(event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target as HTMLElement)?.closest('.cm-editor') ||
(event.target as HTMLElement)?.classList?.contains('cm-content') ||
(event.target as HTMLElement)?.classList?.contains('cm-line'))
) {
return false
}
+17 -4
View File
@@ -40,6 +40,7 @@ export interface ModerationState {
export type ModerationProjectContext = {
project: Labrinth.Projects.v2.Project
scope: 'project'
notifyCopied: (value: string, title: string) => void
}
export type ModerationChecklistContext = {
@@ -54,10 +55,17 @@ export type ModerationTechReviewContext = {
actions: TechReviewActions
}
export type ModerationGlobalContext = {
scope: 'global'
officialUrl: string
notifyCopied: (value: string, title: string) => void
}
export type ModerationContext =
| ModerationProjectContext
| ModerationChecklistContext
| ModerationTechReviewContext
| ModerationGlobalContext
export interface KeybindDefinition {
key: string
@@ -71,7 +79,7 @@ export interface KeybindDefinition {
export type BaseKeybindListener<T> = {
keybind: KeybindDefinition | KeybindDefinition[] | string | string[]
description: string
scope: 'project' | 'checklist' | 'tech-review'
scope: 'project' | 'checklist' | 'tech-review' | 'global'
enabled?: (ctx: T) => boolean
action: (ctx: T) => void
}
@@ -85,10 +93,14 @@ export type KeybindChecklistListener = BaseKeybindListener<ModerationChecklistCo
export type KeybindTechReviewListener = BaseKeybindListener<ModerationTechReviewContext> & {
scope: 'tech-review'
}
export type KeybindGlobalListener = BaseKeybindListener<ModerationGlobalContext> & {
scope: 'global'
}
export type KeybindListener =
| KeybindProjectListener
| KeybindChecklistListener
| KeybindTechReviewListener
| KeybindGlobalListener
export function parseKeybind(keybindString: string): KeybindDefinition {
const parts = keybindString.split('+').map((p) => p.trim().toLowerCase())
@@ -109,12 +121,13 @@ export function normalizeKeybind(keybind: KeybindDefinition | string): KeybindDe
export function matchesKeybind(event: KeyboardEvent, keybind: KeybindDefinition | string): boolean {
const def = normalizeKeybind(keybind)
const wantsMod = !!(def.ctrl || def.meta)
const hasMod = event.ctrlKey || event.metaKey
return (
event.key.toLowerCase() === def.key.toLowerCase() &&
event.ctrlKey === (def.ctrl ?? false) &&
hasMod === wantsMod &&
event.shiftKey === (def.shift ?? false) &&
event.altKey === (def.alt ?? false) &&
event.metaKey === (def.meta ?? false)
event.altKey === (def.alt ?? false)
)
}