feat: register vidbee protocol and respect vidbee:// paths in images (#35)

This commit is contained in:
Nexmoe
2025-12-06 16:45:48 +08:00
committed by GitHub
parent 3946b5ef2f
commit c7815eb81a
8 changed files with 179 additions and 44 deletions

View File

@@ -1,6 +1,8 @@
import { join } from 'node:path'
import { existsSync } from 'node:fs'
import { isAbsolute, join, relative, resolve } from 'node:path'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, type BrowserWindowConstructorOptions, shell } from 'electron'
import { APP_PROTOCOL, APP_PROTOCOL_SCHEME } from '@shared/constants'
import { app, BrowserWindow, type BrowserWindowConstructorOptions, protocol, shell } from 'electron'
import log from 'electron-log/main'
import { autoUpdater } from 'electron-updater'
import appIcon from '../../build/icon.png?asset'
@@ -22,6 +24,19 @@ log.initialize()
// Configure logger settings
configureLogger()
const RENDERER_DIST_PATH = join(__dirname, '../renderer')
protocol.registerSchemesAsPrivileged([
{
scheme: APP_PROTOCOL,
privileges: {
secure: true,
standard: true,
supportFetchAPI: true
}
}
])
let mainWindow: BrowserWindow | null = null
let isQuitting = false
@@ -84,7 +99,7 @@ export function createWindow(): void {
if (process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
mainWindow.loadURL(`${APP_PROTOCOL_SCHEME}renderer/index.html`)
}
mainWindow.webContents.on('did-finish-load', () => {
@@ -117,6 +132,66 @@ function setupDownloadEvents(): void {
})
}
function sanitizeRequestPath(requestUrl: URL): string {
const rawPath = `${requestUrl.hostname}${decodeURIComponent(requestUrl.pathname)}`
const trimmedLeading = rawPath.replace(/^\/+/, '')
const cleaned = trimmedLeading.replace(/\/+$/, '')
return cleaned || 'index.html'
}
function isWithinBase(targetPath: string, basePath: string): boolean {
const relativePath = relative(basePath, targetPath)
return !relativePath.startsWith('..') && !isAbsolute(relativePath)
}
function resolveVidbeeFilePath(requestUrl: URL, userDataPath: string): string | null {
const sanitizedPath = sanitizeRequestPath(requestUrl)
const [rootSegment, ...restSegments] = sanitizedPath.split('/')
const rendererPath = restSegments.join('/') || 'index.html'
if (rootSegment === 'renderer') {
const rendererTarget = resolve(RENDERER_DIST_PATH, rendererPath)
if (isWithinBase(rendererTarget, RENDERER_DIST_PATH) && existsSync(rendererTarget)) {
return rendererTarget
}
}
const userDataTarget = resolve(userDataPath, sanitizedPath)
if (isWithinBase(userDataTarget, userDataPath) && existsSync(userDataTarget)) {
return userDataTarget
}
const rendererFallback = resolve(RENDERER_DIST_PATH, sanitizedPath)
if (isWithinBase(rendererFallback, RENDERER_DIST_PATH) && existsSync(rendererFallback)) {
return rendererFallback
}
return null
}
function registerVidbeeProtocol(): void {
try {
const userDataPath = app.getPath('userData')
protocol.registerFileProtocol(APP_PROTOCOL, (request, callback) => {
const requestUrl = new URL(request.url)
const filePath = resolveVidbeeFilePath(requestUrl, userDataPath)
if (!filePath) {
log.error(`File not found for ${request.url}`)
callback({ error: -6 })
return
}
callback(filePath)
})
} catch (error) {
log.error(`Failed to register ${APP_PROTOCOL} protocol:`, error)
}
}
function initAutoUpdater(): void {
try {
log.info('Initializing auto-updater...')
@@ -189,6 +264,13 @@ app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.vidbee')
registerVidbeeProtocol()
const registered = app.setAsDefaultProtocolClient(APP_PROTOCOL)
if (!registered) {
log.warn(`Failed to register ${APP_PROTOCOL} protocol handler`)
}
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
app.on('browser-window-created', (_, window) => {

View File

@@ -2,7 +2,7 @@ import crypto from 'node:crypto'
import fs from 'node:fs'
import fsPromises from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { APP_PROTOCOL_SCHEME } from '@shared/constants'
import { app } from 'electron'
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif'])
@@ -37,7 +37,11 @@ export class ThumbnailCache {
return null
}
if (originalUrl.startsWith('file://') || originalUrl.startsWith('data:')) {
if (
originalUrl.startsWith(APP_PROTOCOL_SCHEME) ||
originalUrl.startsWith('file://') ||
originalUrl.startsWith('data:')
) {
return originalUrl
}
@@ -59,7 +63,7 @@ export class ThumbnailCache {
const existingPath = await this.findExistingPath(basePath, defaultExtension)
if (existingPath) {
return pathToFileURL(existingPath).toString()
return this.toAppProtocolUrl(existingPath)
}
const response = await fetch(originalUrl)
@@ -75,7 +79,7 @@ export class ThumbnailCache {
const finalPath = `${basePath}${extension}`
await fsPromises.writeFile(finalPath, buffer)
return pathToFileURL(finalPath).toString()
return this.toAppProtocolUrl(finalPath)
} catch (error) {
console.error('Failed to cache thumbnail:', error)
return null
@@ -132,6 +136,13 @@ export class ThumbnailCache {
const basePath = path.join(cacheDir, `${hash}`)
return { basePath, defaultExtension: extension }
}
private toAppProtocolUrl(filePath: string): string {
const userDataPath = app.getPath('userData')
const relativePath = path.relative(userDataPath, filePath).replace(/\\/g, '/')
return `${APP_PROTOCOL_SCHEME}${relativePath}`
}
}
export const thumbnailCache = new ThumbnailCache()