This commit is contained in:
Boof2015
2026-04-22 04:29:22 -04:00
parent 8d8adaa551
commit c0f3f78605
11 changed files with 597 additions and 26 deletions
+40 -1
View File
@@ -1,4 +1,4 @@
import { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, safeStorage, screen, session, shell } from 'electron'
import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, nativeTheme, safeStorage, screen, session, shell } from 'electron'
import type { BrowserWindowConstructorOptions, MenuItemConstructorOptions, OpenDialogOptions, WebContents } from 'electron'
import { execFileSync } from 'child_process'
import { existsSync, readFileSync } from 'fs'
@@ -95,6 +95,7 @@ const NOW_PLAYING_CONFIG_DEFAULTS = {
minHeight: 520,
}
const STATIC_APP_ICON_FILENAME = 'icon.png'
const MAIN_WINDOW_SYNC_SUPPRESSION_MS = 180
const MAIN_WINDOW_VISIBLE_GRAB_MARGIN = 64
const runtimeWindowCapabilities = resolveWindowCapabilities({
@@ -286,6 +287,39 @@ function getWindowStateStore(): FileBackedWindowStateStore {
return windowStateStore
}
function getStaticAppIconPath(): string | undefined {
const candidates = app.isPackaged
? [join(process.resourcesPath, STATIC_APP_ICON_FILENAME)]
: [
join(process.cwd(), 'resources', STATIC_APP_ICON_FILENAME),
join(__dirname, '../../resources', STATIC_APP_ICON_FILENAME),
]
return candidates.find((candidate) => existsSync(candidate))
}
function getStaticWindowIconOptions(): Pick<BrowserWindowConstructorOptions, 'icon'> {
if (process.platform === 'darwin') {
return {}
}
const icon = getStaticAppIconPath()
return icon ? { icon } : {}
}
function applyStaticDockIcon(): void {
if (process.platform !== 'darwin') {
return
}
const iconPath = getStaticAppIconPath()
if (!iconPath) return
const icon = nativeImage.createFromPath(iconPath)
if (icon.isEmpty()) return
app.dock?.setIcon(icon)
}
function broadcastNowPlayingState(state: NowPlayingState): void {
for (const window of BrowserWindow.getAllWindows()) {
if (window.isDestroyed() || window.webContents.isDestroyed()) continue
@@ -895,6 +929,7 @@ async function showCustomDialog(options: DialogOptions): Promise<DialogResult> {
hasShadow: false,
skipTaskbar: true,
show: false,
...getStaticWindowIconOptions(),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
@@ -936,6 +971,7 @@ function createMainWindow(): void {
maximizable: true,
fullscreenable: true,
title: 'Prism',
...getStaticWindowIconOptions(),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
@@ -1105,6 +1141,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro
title: `Prism ${SCOPE_LABELS[kind]}`,
alwaysOnTop: getWindowStateStore().getPopoutAlwaysOnTop(kind),
show: false,
...getStaticWindowIconOptions(),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
@@ -1271,6 +1308,7 @@ function createNowPlayingConfigWindow(): BrowserWindow {
autoHideMenuBar: true,
title: 'Prism Now Playing',
show: false,
...getStaticWindowIconOptions(),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
@@ -1827,6 +1865,7 @@ if (!hasSingleInstanceLock) {
setupIPC()
await getWindowStateStore().initialize()
await syncNativeThemeAppearance()
applyStaticDockIcon()
createMainWindow()
queueProfileOpenPaths(extractProfilePathsFromArgv(process.argv))
void processPendingProfileOpenPaths()
+26
View File
@@ -0,0 +1,26 @@
import type { JSX } from 'react'
interface PrismLogoProps {
className?: string
}
export default function PrismLogo({ className }: PrismLogoProps): JSX.Element {
return (
<svg
className={className}
viewBox="0 0 675 540"
fill="none"
aria-hidden="true"
focusable="false"
>
<path
className="prism-logo__path prism-logo__path--lower"
d="M429.005,240.019c1.742,-1.006 3.718,-1.535 5.729,-1.535c5.527,0 17.412,0 26.401,0c6.328,0 11.459,5.13 11.459,11.459l0,47.969c0,4.094 -2.184,7.877 -5.729,9.923c-29.927,17.278 -167.716,96.831 -188.613,108.895c-1.742,1.006 -3.718,1.535 -5.729,1.535c-9.919,0 -41.456,0 -58.657,0c-6.328,-0 -11.459,-5.13 -11.459,-11.459c0,-9.188 0,-21.759 0,-29.346c-0,-4.094 2.184,-7.877 5.729,-9.923c32.974,-19.038 197.892,-114.253 220.869,-127.519Z"
/>
<path
className="prism-logo__path prism-logo__path--upper"
d="M266.17,121.735c2.011,-0 3.987,0.529 5.729,1.535c17.748,10.247 119.001,68.705 140.796,81.288c2.558,1.477 4.133,4.206 4.133,7.159c0,0.256 0,0.511 0,0.762c0,2.094 -1.121,4.029 -2.938,5.07c-10.641,6.1 -47.292,27.109 -61.53,35.27c-3.541,2.03 -7.893,2.023 -11.428,-0.018c-23.117,-13.347 -109.643,-63.302 -132.798,-76.671c-3.545,-2.047 -5.729,-5.83 -5.729,-9.924c-0,-8.35 -0,-22.847 -0,-33.013c0,-6.328 5.13,-11.459 11.459,-11.459c15.744,0 43.157,0 52.305,0Z"
/>
</svg>
)
}
+22 -4
View File
@@ -4,6 +4,7 @@ import { useSettingsStore } from '../stores/settingsStore'
import { useUpdateStore } from '../stores/updateStore'
import { useUiStore } from '../stores/uiStore'
import { getRendererWindowCapabilities } from '../windowCapabilities'
import PrismLogo from './PrismLogo'
function SettingsIcon(): JSX.Element {
return (
@@ -81,6 +82,7 @@ interface ToolbarProps {
}
const DEFAULT_PROFILE_ID = 'profile_default'
const PRISM_SUPPORT_URL = 'https://ko-fi.com/boof2015'
const WAYLAND_REPOSITION_UNAVAILABLE_MESSAGE = 'Top/bottom repositioning is unavailable on native Wayland.'
function isToolbarInteractiveTarget(target: EventTarget | null): boolean {
@@ -409,6 +411,16 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps):
void openReleasesPage()
}, [openReleasesPage])
const handleOpenSupport = useCallback(() => {
void window.electronAPI.openExternalUrl(PRISM_SUPPORT_URL).catch((error) => {
showBanner({
tone: 'error',
message: getErrorMessage(error, 'Could not open Ko-fi.'),
actions: [],
})
})
}, [showBanner])
const handleReposition = useCallback((position: 'top' | 'bottom') => {
if (!supportsProgrammaticReposition) {
return
@@ -495,12 +507,18 @@ export default function Toolbar({ onOpenSettings, settingsOpen }: ToolbarProps):
<GripIcon />
</button>
<div
<button
type="button"
className="toolbar__brand"
onClick={handleOpenSupport}
title="Support Prism on Ko-fi"
aria-label="Support Prism on Ko-fi"
>
<span className="toolbar__brand-mark" />
<span className="toolbar__brand-text">Prism</span>
</div>
<span className="toolbar__brand-logo">
<PrismLogo />
</span>
<span className="toolbar__brand-heart" aria-hidden="true" />
</button>
<AppVersionBlock
buildInfo={appBuildInfo}
+120 -20
View File
@@ -332,27 +332,119 @@ select {
}
.toolbar__brand {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 86px;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid var(--control-border-active);
background: linear-gradient(180deg, var(--control-bg-active), var(--panel-surface));
box-shadow: inset 0 1px 0 var(--glass-highlight);
justify-content: center;
width: 36px;
height: 28px;
padding: 2px;
margin: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: inherit;
line-height: 0;
cursor: pointer;
flex-shrink: 0;
transition: background 0.15s ease, color 0.15s ease;
-webkit-app-region: no-drag;
}
.toolbar__brand-mark {
width: 6px;
height: 6px;
border-radius: 999px;
background: rgba(var(--accent-rgb), 0.92);
box-shadow: 0 0 10px rgba(var(--accent-rgb), 0.32);
.toolbar__brand:hover {
background: var(--glass-bg);
color: var(--text-primary);
}
.toolbar__brand-logo {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 21px;
flex-shrink: 0;
transition: opacity 0.15s ease, transform 0.2s ease;
}
.toolbar__brand-logo > svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
}
.prism-logo__path {
transition: fill 0.15s ease, filter 0.15s ease;
}
.prism-logo__path--lower {
fill: var(--accent);
filter: drop-shadow(0 0 5px var(--accent-glow));
}
.prism-logo__path--upper {
fill: var(--accent);
filter: drop-shadow(0 0 4px var(--accent-glow));
}
.toolbar__brand-heart {
width: 2px;
height: 2px;
margin-top: -4px;
margin-left: -5px;
background: #fb7185;
box-shadow:
2px 0 0 #fb7185,
6px 0 0 #fb7185,
8px 0 0 #fb7185,
0 2px 0 #fb7185,
2px 2px 0 #fb7185,
4px 2px 0 #fb7185,
6px 2px 0 #fb7185,
8px 2px 0 #fb7185,
2px 4px 0 #fb7185,
4px 4px 0 #fb7185,
6px 4px 0 #fb7185,
4px 6px 0 #fb7185;
opacity: 0;
transform: translate3d(0, 2px, 0);
transform-origin: center;
transition: opacity 0.15s ease, transform 0.15s ease;
filter: drop-shadow(0 0 4px rgba(248, 113, 113, 0.45));
pointer-events: none;
position: absolute;
top: 50%;
left: 50%;
}
.toolbar__brand:hover .toolbar__brand-heart {
opacity: 1;
transform: translate3d(0, 0, 0);
animation: titlebarLogoHeartPop 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
.toolbar__brand:hover .toolbar__brand-logo {
opacity: 0;
transform: scale(0.82);
}
@keyframes titlebarLogoHeartPop {
0% {
opacity: 0;
transform: translate3d(0, 3px, 0);
}
62% {
opacity: 1;
transform: translate3d(0, -1px, 0);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.toolbar__brand-text,
.toolbar__chip,
.toolbar__icon-button,
.settings-section-title,
@@ -367,11 +459,6 @@ select {
text-transform: uppercase;
}
.toolbar__brand-text {
color: var(--text-secondary);
font-size: 10px;
}
.toolbar__version {
display: inline-flex;
align-items: center;
@@ -2018,6 +2105,7 @@ button.toolbar__version:hover {
transform: none;
}
.toolbar__brand:focus-visible,
.toolbar__profile-button:focus-visible,
.toolbar__chip:focus-visible,
.toolbar__icon-button:focus-visible,
@@ -2035,6 +2123,18 @@ button.toolbar__version:hover {
0 0 0 4px rgba(var(--accent-rgb), 0.12);
}
@media (prefers-reduced-motion: reduce) {
.toolbar__brand,
.toolbar__brand-logo,
.toolbar__brand-heart {
transition-duration: 0.08s !important;
}
.toolbar__brand:hover .toolbar__brand-heart {
animation: none !important;
}
}
.scope-popout__settings-panel {
height: 100%;
overflow-y: auto;