mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
Introduce SDK 0.4, manifest v3, protocol v4, the isolated plugin runtime, and cpr-plugin packaging CLI for independently maintained plugins. Support installation, permissions, one active configuration per plugin, version-specific settings, rollback, management pages, and CLI extensions. Isolate plugin failures so native gateway startup and requests remain available. Keep provider registration and UI choices fixed to built-in OpenAI and xAI. Support nine extension capabilities with permission-scoped model and account callbacks to the host. Unify plugin settings and version management, support plugin icons, and improve narrow layouts. Use the fixed GitHub UI dependency and pnpm 12 for standalone frontend and container builds. Reduce runtime test packaging cost and align preparation modules with the repository layout. Co-authored-by: Codex <noreply@openai.com>
320 lines
9.6 KiB
TypeScript
320 lines
9.6 KiB
TypeScript
// @env browser
|
|
|
|
import type {
|
|
ResolvedTheme,
|
|
ThemeColorId,
|
|
ThemeCustomization,
|
|
ThemeMode,
|
|
} from '@codex-proxy/ui/theme'
|
|
|
|
import { normalizeHexColor } from '@codex-proxy/ui'
|
|
import {
|
|
applyResolvedTheme,
|
|
DEFAULT_CUSTOM_THEME_COLOR,
|
|
DEFAULT_THEME_COLOR,
|
|
DEFAULT_THEME_MODE,
|
|
isThemeColorId,
|
|
isThemeMode,
|
|
normalizeThemeCustomization,
|
|
resolveTheme,
|
|
resolveThemeName,
|
|
} from '@codex-proxy/ui/theme'
|
|
import { usePreferredDark, usePreferredReducedMotion, useTimeoutFn } from '@vueuse/core'
|
|
|
|
import { defineStore } from 'pinia'
|
|
import { computed, ref, shallowRef, watch } from 'vue'
|
|
|
|
interface ThemeTransitionOrigin {
|
|
x: number
|
|
y: number
|
|
}
|
|
|
|
interface ThemeConfigurationInput {
|
|
mode: ThemeMode
|
|
color: ThemeColorId
|
|
customColor: string
|
|
customization: ThemeCustomization
|
|
}
|
|
|
|
interface ViewTransition {
|
|
ready: Promise<void>
|
|
finished: Promise<void>
|
|
}
|
|
|
|
type ViewTransitionDocument = Document & {
|
|
startViewTransition?: (callback: () => void) => ViewTransition
|
|
}
|
|
|
|
export const useThemeStore = defineStore(
|
|
'theme',
|
|
() => {
|
|
const themeMode = shallowRef<ThemeMode>(DEFAULT_THEME_MODE)
|
|
const themeColor = shallowRef<ThemeColorId>(DEFAULT_THEME_COLOR)
|
|
const customThemeColor = shallowRef(DEFAULT_CUSTOM_THEME_COLOR)
|
|
const themeCustomization = ref<ThemeCustomization>({})
|
|
const themeRevision = shallowRef(0)
|
|
const themeTransitioning = shallowRef(false)
|
|
const preferredDark = usePreferredDark()
|
|
const preferredMotion = usePreferredReducedMotion()
|
|
let appliedThemeSignature: string | undefined
|
|
let themeTransitionOrigin: ThemeTransitionOrigin | undefined
|
|
let themeTransitionRequested = false
|
|
|
|
const effectiveTheme = computed(() => resolveThemeName(themeMode.value, preferredDark.value))
|
|
const resolvedTheme = computed(() =>
|
|
resolveTheme(
|
|
effectiveTheme.value,
|
|
themeColor.value,
|
|
customThemeColor.value,
|
|
themeCustomization.value,
|
|
),
|
|
)
|
|
|
|
const { start: startFallbackTransitionTimer, stop: stopFallbackTransitionTimer } = useTimeoutFn(
|
|
() => {
|
|
document.documentElement.classList.remove('theme-fallback-transition')
|
|
themeTransitioning.value = false
|
|
},
|
|
180,
|
|
{ immediate: false },
|
|
)
|
|
|
|
watch(resolvedTheme, theme => applyTheme(theme))
|
|
|
|
function initializeTheme(): void {
|
|
normalizePersistedTheme()
|
|
applyTheme(resolvedTheme.value)
|
|
}
|
|
|
|
function normalizePersistedTheme(): void {
|
|
if (!isThemeMode(themeMode.value))
|
|
themeMode.value = DEFAULT_THEME_MODE
|
|
if (!isThemeColorId(themeColor.value))
|
|
themeColor.value = DEFAULT_THEME_COLOR
|
|
customThemeColor.value = normalizeHexColor(customThemeColor.value) ?? DEFAULT_CUSTOM_THEME_COLOR
|
|
themeCustomization.value = normalizeThemeCustomization(themeCustomization.value)
|
|
}
|
|
|
|
function applyTheme(theme: ResolvedTheme): void {
|
|
if (typeof document === 'undefined')
|
|
return
|
|
|
|
const signature = themeSignature(theme)
|
|
if (appliedThemeSignature === signature) {
|
|
clearThemeTransitionRequest()
|
|
return
|
|
}
|
|
|
|
if (
|
|
appliedThemeSignature === undefined
|
|
|| !themeTransitionRequested
|
|
|| preferredMotion.value === 'reduce'
|
|
|| themeTransitioning.value
|
|
) {
|
|
commitTheme(theme)
|
|
clearThemeTransitionRequest()
|
|
return
|
|
}
|
|
|
|
runThemeTransition(theme)
|
|
clearThemeTransitionRequest()
|
|
}
|
|
|
|
function commitTheme(theme: ResolvedTheme): void {
|
|
applyResolvedTheme(document.documentElement, theme)
|
|
appliedThemeSignature = themeSignature(theme)
|
|
themeRevision.value += 1
|
|
}
|
|
|
|
function runThemeTransition(theme: ResolvedTheme): void {
|
|
const transitionDocument = document as ViewTransitionDocument
|
|
const shrinkingDarkLayer
|
|
= document.documentElement.dataset.theme === 'dark' && theme.name === 'light'
|
|
|
|
if (!transitionDocument.startViewTransition) {
|
|
runFallbackThemeTransition(theme)
|
|
return
|
|
}
|
|
|
|
const origin = themeTransitionOrigin ?? {
|
|
x: window.innerWidth - 44,
|
|
y: 44,
|
|
}
|
|
const maxX = Math.max(origin.x, window.innerWidth - origin.x)
|
|
const maxY = Math.max(origin.y, window.innerHeight - origin.y)
|
|
const radius = Math.hypot(maxX, maxY)
|
|
document.documentElement.classList.toggle('theme-view-transition-shrink', shrinkingDarkLayer)
|
|
themeTransitioning.value = true
|
|
let transition: ViewTransition
|
|
try {
|
|
transition = transitionDocument.startViewTransition(() => commitTheme(theme))
|
|
}
|
|
catch {
|
|
document.documentElement.classList.remove('theme-view-transition-shrink')
|
|
themeTransitioning.value = false
|
|
runFallbackThemeTransition(theme)
|
|
return
|
|
}
|
|
|
|
let activeAnimation: Animation | undefined
|
|
void transition.finished
|
|
.catch(() => undefined)
|
|
.finally(() => {
|
|
// 旧快照移除后再取消裁剪,避免深色图层在最后一帧重新铺满页面。
|
|
activeAnimation?.cancel()
|
|
document.documentElement.classList.remove('theme-view-transition-shrink')
|
|
themeTransitioning.value = false
|
|
})
|
|
void transition.ready
|
|
.then(() => {
|
|
activeAnimation = document.documentElement.animate(
|
|
{
|
|
clipPath: shrinkingDarkLayer
|
|
? [
|
|
`circle(${radius}px at ${origin.x}px ${origin.y}px)`,
|
|
`circle(0px at ${origin.x}px ${origin.y}px)`,
|
|
]
|
|
: [
|
|
`circle(0px at ${origin.x}px ${origin.y}px)`,
|
|
`circle(${radius}px at ${origin.x}px ${origin.y}px)`,
|
|
],
|
|
},
|
|
{
|
|
duration: 420,
|
|
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
fill: 'both',
|
|
pseudoElement: shrinkingDarkLayer
|
|
? '::view-transition-old(root)'
|
|
: '::view-transition-new(root)',
|
|
},
|
|
)
|
|
})
|
|
.catch(() => undefined)
|
|
}
|
|
|
|
function runFallbackThemeTransition(theme: ResolvedTheme): void {
|
|
themeTransitioning.value = true
|
|
stopFallbackTransitionTimer()
|
|
document.documentElement.classList.add('theme-fallback-transition')
|
|
commitTheme(theme)
|
|
startFallbackTransitionTimer()
|
|
}
|
|
|
|
function setThemeMode(mode: ThemeMode, event?: MouseEvent): void {
|
|
if (mode === themeMode.value) {
|
|
clearThemeTransitionRequest()
|
|
return
|
|
}
|
|
requestThemeTransition(event)
|
|
themeMode.value = mode
|
|
}
|
|
|
|
function setThemeColor(color: ThemeColorId, event?: MouseEvent): void {
|
|
if (color === themeColor.value) {
|
|
clearThemeTransitionRequest()
|
|
return
|
|
}
|
|
requestThemeTransition(event)
|
|
themeColor.value = color
|
|
}
|
|
|
|
function setCustomThemeColor(color: string, event?: MouseEvent): boolean {
|
|
const normalized = normalizeHexColor(color)
|
|
if (!normalized)
|
|
return false
|
|
if (themeColor.value === 'custom' && customThemeColor.value === normalized) {
|
|
clearThemeTransitionRequest()
|
|
return true
|
|
}
|
|
requestThemeTransition(event)
|
|
customThemeColor.value = normalized
|
|
themeColor.value = 'custom'
|
|
return true
|
|
}
|
|
|
|
function setThemeConfiguration(
|
|
configuration: ThemeConfigurationInput,
|
|
event?: MouseEvent,
|
|
): boolean {
|
|
const customColor = normalizeHexColor(configuration.customColor)
|
|
if (!isThemeMode(configuration.mode) || !isThemeColorId(configuration.color) || !customColor)
|
|
return false
|
|
|
|
requestThemeTransition(event)
|
|
themeMode.value = configuration.mode
|
|
themeColor.value = configuration.color
|
|
customThemeColor.value = customColor
|
|
themeCustomization.value = normalizeThemeCustomization(configuration.customization)
|
|
return true
|
|
}
|
|
|
|
function resetTheme(event?: MouseEvent): void {
|
|
const unchanged
|
|
= themeMode.value === DEFAULT_THEME_MODE
|
|
&& themeColor.value === DEFAULT_THEME_COLOR
|
|
&& customThemeColor.value === DEFAULT_CUSTOM_THEME_COLOR
|
|
&& Object.keys(themeCustomization.value).length === 0
|
|
if (unchanged) {
|
|
clearThemeTransitionRequest()
|
|
return
|
|
}
|
|
requestThemeTransition(event)
|
|
themeMode.value = DEFAULT_THEME_MODE
|
|
themeColor.value = DEFAULT_THEME_COLOR
|
|
customThemeColor.value = DEFAULT_CUSTOM_THEME_COLOR
|
|
themeCustomization.value = {}
|
|
}
|
|
|
|
function toggleTheme(event?: MouseEvent): void {
|
|
if (themeTransitioning.value)
|
|
return
|
|
requestThemeTransition(event)
|
|
themeMode.value = effectiveTheme.value === 'dark' ? 'light' : 'dark'
|
|
}
|
|
|
|
function requestThemeTransition(event?: MouseEvent): void {
|
|
themeTransitionOrigin = event
|
|
? { x: event.clientX, y: event.clientY }
|
|
: undefined
|
|
themeTransitionRequested = Boolean(event)
|
|
}
|
|
|
|
function clearThemeTransitionRequest(): void {
|
|
themeTransitionOrigin = undefined
|
|
themeTransitionRequested = false
|
|
}
|
|
|
|
return {
|
|
themeMode,
|
|
themeColor,
|
|
customThemeColor,
|
|
themeCustomization,
|
|
themeRevision,
|
|
themeTransitioning,
|
|
effectiveTheme,
|
|
initializeTheme,
|
|
setThemeMode,
|
|
setThemeColor,
|
|
setCustomThemeColor,
|
|
setThemeConfiguration,
|
|
resetTheme,
|
|
toggleTheme,
|
|
}
|
|
},
|
|
{
|
|
persist: {
|
|
key: 'codex-proxy-rs-theme',
|
|
pick: [
|
|
'themeMode',
|
|
'themeColor',
|
|
'customThemeColor',
|
|
'themeCustomization',
|
|
],
|
|
},
|
|
},
|
|
)
|
|
|
|
function themeSignature(theme: ResolvedTheme): string {
|
|
return `${theme.name}:${theme.colorId}:${theme.seed}:${JSON.stringify(theme.tokens)}`
|
|
}
|