Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee717888f | ||
|
|
c7815eb81a | ||
|
|
3946b5ef2f | ||
|
|
ef99eb1f59 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,7 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
out
|
out
|
||||||
|
.conductor/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintcache
|
.eslintcache
|
||||||
*.log*
|
*.log*
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ appId: com.vidbee
|
|||||||
productName: VidBee
|
productName: VidBee
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
|
protocols:
|
||||||
|
- name: VidBee
|
||||||
|
schemes:
|
||||||
|
- vidbee
|
||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!**/.vscode/*'
|
||||||
- '!src/*'
|
- '!src/*'
|
||||||
@@ -19,6 +23,7 @@ nsis:
|
|||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
mac:
|
mac:
|
||||||
|
identity: null
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
notarize: false
|
notarize: false
|
||||||
artifactName: ${name}-${version}-${arch}.${ext}
|
artifactName: ${name}-${version}-${arch}.${ext}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vidbee",
|
"name": "vidbee",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "A modern Electron application for downloading videos and audios",
|
"description": "A modern Electron application for downloading videos and audios",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "VidBee",
|
"author": "VidBee",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import type { AppSettings, DownloadOptions } from '../../shared/types'
|
import type { AppSettings, DownloadOptions } from '../../shared/types'
|
||||||
|
import { resolvePathWithHome } from '../utils/path-helpers'
|
||||||
|
|
||||||
export const sanitizeFilenameTemplate = (template: string): string => {
|
export const sanitizeFilenameTemplate = (template: string): string => {
|
||||||
const trimmed = template.trim()
|
const trimmed = template.trim()
|
||||||
@@ -115,8 +116,9 @@ export const buildDownloadArgs = (
|
|||||||
args.push('--proxy', settings.proxy)
|
args.push('--proxy', settings.proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.configPath) {
|
const configPath = resolvePathWithHome(settings.configPath)
|
||||||
args.push('--config-location', settings.configPath)
|
if (configPath) {
|
||||||
|
args.push('--config-location', configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push(options.url)
|
args.push(options.url)
|
||||||
|
|||||||
@@ -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 { 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 log from 'electron-log/main'
|
||||||
import { autoUpdater } from 'electron-updater'
|
import { autoUpdater } from 'electron-updater'
|
||||||
import appIcon from '../../build/icon.png?asset'
|
import appIcon from '../../build/icon.png?asset'
|
||||||
@@ -22,6 +24,19 @@ log.initialize()
|
|||||||
// Configure logger settings
|
// Configure logger settings
|
||||||
configureLogger()
|
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 mainWindow: BrowserWindow | null = null
|
||||||
let isQuitting = false
|
let isQuitting = false
|
||||||
|
|
||||||
@@ -84,7 +99,7 @@ export function createWindow(): void {
|
|||||||
if (process.env.ELECTRON_RENDERER_URL) {
|
if (process.env.ELECTRON_RENDERER_URL) {
|
||||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
|
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
mainWindow.loadURL(`${APP_PROTOCOL_SCHEME}renderer/index.html`)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
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 {
|
function initAutoUpdater(): void {
|
||||||
try {
|
try {
|
||||||
log.info('Initializing auto-updater...')
|
log.info('Initializing auto-updater...')
|
||||||
@@ -189,6 +264,13 @@ app.whenReady().then(async () => {
|
|||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('com.vidbee')
|
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
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '../download-engine/format-utils'
|
} from '../download-engine/format-utils'
|
||||||
import { settingsManager } from '../settings'
|
import { settingsManager } from '../settings'
|
||||||
import { scopedLoggers } from '../utils/logger'
|
import { scopedLoggers } from '../utils/logger'
|
||||||
|
import { resolvePathWithHome } from '../utils/path-helpers'
|
||||||
import { DownloadQueue } from './download-queue'
|
import { DownloadQueue } from './download-queue'
|
||||||
import { ffmpegManager } from './ffmpeg-manager'
|
import { ffmpegManager } from './ffmpeg-manager'
|
||||||
import { historyManager } from './history-manager'
|
import { historyManager } from './history-manager'
|
||||||
@@ -74,8 +75,9 @@ class DownloadEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add config file if configured
|
// Add config file if configured
|
||||||
if (settings.configPath) {
|
const configPath = resolvePathWithHome(settings.configPath)
|
||||||
args.push('--config-location', `"${settings.configPath}"`)
|
if (configPath) {
|
||||||
|
args.push('--config-location', configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push(url)
|
args.push(url)
|
||||||
@@ -169,8 +171,9 @@ class DownloadEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add config file if configured
|
// Add config file if configured
|
||||||
if (settings.configPath) {
|
const configPath = resolvePathWithHome(settings.configPath)
|
||||||
args.push('--config-location', `"${settings.configPath}"`)
|
if (configPath) {
|
||||||
|
args.push('--config-location', configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push(url)
|
args.push(url)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto'
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import fsPromises from 'node:fs/promises'
|
import fsPromises from 'node:fs/promises'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { APP_PROTOCOL_SCHEME } from '@shared/constants'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
|
||||||
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif'])
|
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif'])
|
||||||
@@ -37,7 +37,11 @@ export class ThumbnailCache {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (originalUrl.startsWith('file://') || originalUrl.startsWith('data:')) {
|
if (
|
||||||
|
originalUrl.startsWith(APP_PROTOCOL_SCHEME) ||
|
||||||
|
originalUrl.startsWith('file://') ||
|
||||||
|
originalUrl.startsWith('data:')
|
||||||
|
) {
|
||||||
return originalUrl
|
return originalUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ export class ThumbnailCache {
|
|||||||
|
|
||||||
const existingPath = await this.findExistingPath(basePath, defaultExtension)
|
const existingPath = await this.findExistingPath(basePath, defaultExtension)
|
||||||
if (existingPath) {
|
if (existingPath) {
|
||||||
return pathToFileURL(existingPath).toString()
|
return this.toAppProtocolUrl(existingPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(originalUrl)
|
const response = await fetch(originalUrl)
|
||||||
@@ -75,7 +79,7 @@ export class ThumbnailCache {
|
|||||||
const finalPath = `${basePath}${extension}`
|
const finalPath = `${basePath}${extension}`
|
||||||
|
|
||||||
await fsPromises.writeFile(finalPath, buffer)
|
await fsPromises.writeFile(finalPath, buffer)
|
||||||
return pathToFileURL(finalPath).toString()
|
return this.toAppProtocolUrl(finalPath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to cache thumbnail:', error)
|
console.error('Failed to cache thumbnail:', error)
|
||||||
return null
|
return null
|
||||||
@@ -132,6 +136,13 @@ export class ThumbnailCache {
|
|||||||
const basePath = path.join(cacheDir, `${hash}`)
|
const basePath = path.join(cacheDir, `${hash}`)
|
||||||
return { basePath, defaultExtension: extension }
|
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()
|
export const thumbnailCache = new ThumbnailCache()
|
||||||
|
|||||||
19
src/main/utils/path-helpers.ts
Normal file
19
src/main/utils/path-helpers.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export const resolvePathWithHome = (rawPath?: string | null): string | undefined => {
|
||||||
|
const trimmed = rawPath?.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed === '~') {
|
||||||
|
return os.homedir()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
||||||
|
return path.join(os.homedir(), trimmed.slice(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>VidBee</title>
|
<title>VidBee</title>
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self' 'unsafe-eval' https://rybbit.102417.xyz; style-src 'self' 'unsafe-inline'; img-src 'self' data: file: https://i.ytimg.com https://img.youtube.com; connect-src 'self' https://rybbit.102417.xyz" />
|
content="default-src 'self'; script-src 'self' 'unsafe-eval' https://rybbit.102417.xyz; style-src 'self' 'unsafe-inline'; img-src 'self' data: file: vidbee: https://i.ytimg.com https://img.youtube.com; connect-src 'self' https://rybbit.102417.xyz" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useAtom, useSetAtom } from 'jotai'
|
|||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { ipcEvents, ipcServices } from './lib/ipc'
|
import { ipcEvents, ipcServices } from './lib/ipc'
|
||||||
import { About } from './pages/About'
|
import { About } from './pages/About'
|
||||||
@@ -19,8 +20,36 @@ import { loadSubscriptionsAtom, setSubscriptionsAtom } from './store/subscriptio
|
|||||||
|
|
||||||
type Page = 'home' | 'subscriptions' | 'settings' | 'about' | 'sites'
|
type Page = 'home' | 'subscriptions' | 'settings' | 'about' | 'sites'
|
||||||
|
|
||||||
|
const pageToPath: Record<Page, string> = {
|
||||||
|
home: '/',
|
||||||
|
subscriptions: '/subscriptions',
|
||||||
|
settings: '/settings',
|
||||||
|
about: '/about',
|
||||||
|
sites: '/sites'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizePathname = (pathname: string): string => {
|
||||||
|
const trimmed = pathname.replace(/\/+$/, '')
|
||||||
|
return trimmed === '' ? '/' : trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathToPage = (pathname: string): Page => {
|
||||||
|
const normalized = normalizePathname(pathname)
|
||||||
|
switch (normalized) {
|
||||||
|
case '/subscriptions':
|
||||||
|
return 'subscriptions'
|
||||||
|
case '/settings':
|
||||||
|
return 'settings'
|
||||||
|
case '/about':
|
||||||
|
return 'about'
|
||||||
|
case '/sites':
|
||||||
|
return 'sites'
|
||||||
|
default:
|
||||||
|
return 'home'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const [currentPage, setCurrentPage] = useState<Page>('home')
|
|
||||||
const [platform, setPlatform] = useState<string>('')
|
const [platform, setPlatform] = useState<string>('')
|
||||||
const loadSubscriptions = useSetAtom(loadSubscriptionsAtom)
|
const loadSubscriptions = useSetAtom(loadSubscriptionsAtom)
|
||||||
const setSubscriptions = useSetAtom(setSubscriptionsAtom)
|
const setSubscriptions = useSetAtom(setSubscriptionsAtom)
|
||||||
@@ -29,6 +58,16 @@ function AppContent() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const updateDownloadInProgressRef = useRef(false)
|
const updateDownloadInProgressRef = useRef(false)
|
||||||
const analyticsScriptRef = useRef<HTMLScriptElement | null>(null)
|
const analyticsScriptRef = useRef<HTMLScriptElement | null>(null)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const currentPage = pathToPage(location.pathname)
|
||||||
|
|
||||||
|
const handlePageChange = (page: Page) => {
|
||||||
|
const targetPath = pageToPath[page] ?? '/'
|
||||||
|
if (normalizePathname(location.pathname) !== targetPath) {
|
||||||
|
navigate(targetPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings()
|
loadSettings()
|
||||||
@@ -185,37 +224,10 @@ function AppContent() {
|
|||||||
}
|
}
|
||||||
}, [t])
|
}, [t])
|
||||||
|
|
||||||
const renderPage = () => {
|
|
||||||
switch (currentPage) {
|
|
||||||
case 'home':
|
|
||||||
return (
|
|
||||||
<Home
|
|
||||||
onOpenSupportedSites={() => setCurrentPage('sites')}
|
|
||||||
onOpenSettings={() => setCurrentPage('settings')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'settings':
|
|
||||||
return <Settings />
|
|
||||||
case 'subscriptions':
|
|
||||||
return <Subscriptions />
|
|
||||||
case 'about':
|
|
||||||
return <About />
|
|
||||||
case 'sites':
|
|
||||||
return <SupportedSites />
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Home
|
|
||||||
onOpenSupportedSites={() => setCurrentPage('sites')}
|
|
||||||
onOpenSettings={() => setCurrentPage('settings')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row h-screen">
|
<div className="flex flex-row h-screen">
|
||||||
{/* Sidebar Navigation */}
|
{/* Sidebar Navigation */}
|
||||||
<Sidebar currentPage={currentPage} onPageChange={setCurrentPage} />
|
<Sidebar currentPage={currentPage} onPageChange={handlePageChange} />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex flex-col flex-1 min-h-0 overflow-hidden bg-background">
|
<main className="flex flex-col flex-1 min-h-0 overflow-hidden bg-background">
|
||||||
@@ -227,7 +239,22 @@ function AppContent() {
|
|||||||
style={{ maxWidth: '100%' }}
|
style={{ maxWidth: '100%' }}
|
||||||
>
|
>
|
||||||
<div className="w-full overflow-hidden" style={{ maxWidth: '100%' }}>
|
<div className="w-full overflow-hidden" style={{ maxWidth: '100%' }}>
|
||||||
{renderPage()}
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<Home
|
||||||
|
onOpenSupportedSites={() => handlePageChange('sites')}
|
||||||
|
onOpenSettings={() => handlePageChange('settings')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/subscriptions" element={<Subscriptions />} />
|
||||||
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
<Route path="/about" element={<About />} />
|
||||||
|
<Route path="/sites" element={<SupportedSites />} />
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</main>
|
</main>
|
||||||
@@ -240,7 +267,9 @@ function AppContent() {
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<AppContent />
|
<HashRouter>
|
||||||
|
<AppContent />
|
||||||
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { APP_PROTOCOL_SCHEME } from '@shared/constants'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useCachedThumbnail } from '../../hooks/use-cached-thumbnail'
|
import { useCachedThumbnail } from '../../hooks/use-cached-thumbnail'
|
||||||
@@ -7,7 +8,7 @@ interface RemoteImageProps {
|
|||||||
/**
|
/**
|
||||||
* Remote image URL or local file path
|
* Remote image URL or local file path
|
||||||
* If it's a remote URL (http/https), it will be cached automatically
|
* If it's a remote URL (http/https), it will be cached automatically
|
||||||
* If it's a local path (file:// or data:), it will be used directly
|
* If it's a local path (vidbee://, file:// or data:), it will be used directly
|
||||||
*/
|
*/
|
||||||
src?: string | null
|
src?: string | null
|
||||||
alt: string
|
alt: string
|
||||||
@@ -36,7 +37,7 @@ interface RemoteImageProps {
|
|||||||
*
|
*
|
||||||
* This component automatically handles:
|
* This component automatically handles:
|
||||||
* - Remote URL caching via thumbnail cache service
|
* - Remote URL caching via thumbnail cache service
|
||||||
* - Local file paths (file://, data:)
|
* - Local file paths (vidbee://, file://, data:)
|
||||||
* - Loading states and error handling
|
* - Loading states and error handling
|
||||||
* - Placeholder display with loading indicator
|
* - Placeholder display with loading indicator
|
||||||
*
|
*
|
||||||
@@ -46,7 +47,7 @@ interface RemoteImageProps {
|
|||||||
* <RemoteImage src="https://example.com/image.jpg" alt="Example" />
|
* <RemoteImage src="https://example.com/image.jpg" alt="Example" />
|
||||||
*
|
*
|
||||||
* // Local file path (no caching)
|
* // Local file path (no caching)
|
||||||
* <RemoteImage src="file:///path/to/image.jpg" alt="Local" />
|
* <RemoteImage src="vidbee://thumbnails/example.jpg" alt="Local" />
|
||||||
*
|
*
|
||||||
* // Without cache (direct load)
|
* // Without cache (direct load)
|
||||||
* <RemoteImage src="https://example.com/image.jpg" alt="Example" useCache={false} />
|
* <RemoteImage src="https://example.com/image.jpg" alt="Example" useCache={false} />
|
||||||
@@ -80,6 +81,7 @@ export function RemoteImage({
|
|||||||
const shouldUseCache =
|
const shouldUseCache =
|
||||||
useCache &&
|
useCache &&
|
||||||
src &&
|
src &&
|
||||||
|
!src.startsWith(APP_PROTOCOL_SCHEME) &&
|
||||||
!src.startsWith('file://') &&
|
!src.startsWith('file://') &&
|
||||||
!src.startsWith('data:') &&
|
!src.startsWith('data:') &&
|
||||||
(src.startsWith('http://') || src.startsWith('https://'))
|
(src.startsWith('http://') || src.startsWith('https://'))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { APP_PROTOCOL_SCHEME } from '@shared/constants'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { ipcServices } from '../lib/ipc'
|
import { ipcServices } from '../lib/ipc'
|
||||||
|
|
||||||
export const useCachedThumbnail = (url?: string | null): string | undefined => {
|
export const useCachedThumbnail = (url?: string | null): string | undefined => {
|
||||||
@@ -14,7 +14,11 @@ export const useCachedThumbnail = (url?: string | null): string | undefined => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.startsWith('file://') || url.startsWith('data:')) {
|
if (
|
||||||
|
url.startsWith(APP_PROTOCOL_SCHEME) ||
|
||||||
|
url.startsWith('file://') ||
|
||||||
|
url.startsWith('data:')
|
||||||
|
) {
|
||||||
setCachedUrl(url)
|
setCachedUrl(url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,6 +322,7 @@
|
|||||||
"browserOptions": {
|
"browserOptions": {
|
||||||
"brave": "Brave",
|
"brave": "Brave",
|
||||||
"chrome": "Chrome",
|
"chrome": "Chrome",
|
||||||
|
"chromium": "Chromium",
|
||||||
"edge": "Edge",
|
"edge": "Edge",
|
||||||
"firefox": "Firefox",
|
"firefox": "Firefox",
|
||||||
"safari": "Safari"
|
"safari": "Safari"
|
||||||
|
|||||||
@@ -436,6 +436,9 @@ export function Settings() {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">{t('settings.none')}</SelectItem>
|
<SelectItem value="none">{t('settings.none')}</SelectItem>
|
||||||
<SelectItem value="chrome">{t('settings.browserOptions.chrome')}</SelectItem>
|
<SelectItem value="chrome">{t('settings.browserOptions.chrome')}</SelectItem>
|
||||||
|
<SelectItem value="chromium">
|
||||||
|
{t('settings.browserOptions.chromium')}
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="firefox">
|
<SelectItem value="firefox">
|
||||||
{t('settings.browserOptions.firefox')}
|
{t('settings.browserOptions.firefox')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
2
src/shared/constants.ts
Normal file
2
src/shared/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const APP_PROTOCOL = 'vidbee'
|
||||||
|
export const APP_PROTOCOL_SCHEME = `${APP_PROTOCOL}://`
|
||||||
Reference in New Issue
Block a user