Compare commits

..

3 Commits

Author SHA1 Message Date
Nexmoe
6c3f070797 chore: release v1.0.0 2025-11-15 12:30:16 +08:00
Nexmoe
62a339e5b9 feat: rss-subscriptions (#28)
* feat(subscriptions): add subscription atoms and types for feeds and rules

* refactor(types): remove obsolete format/quality/codec fields and related logic to simplify download and avoid redundant storage

* feat: add download_history schema, migration, and tag utils

* refactor(dialog): convert to functional wrappers, simplify overlay and

* feat(rss): add enclosure support, refine thumbnail lookup and UI labels
feat(settings): add toggleable anonymous analytics collection and loader script

* feat(settings): add toggleable anonymous analytics collection and loader script

* feat(i18n): add subscriptions UI strings and import downloads types

* refactor(subscriptions): remove legacy status fields and migrate items ordering

* feat: add Radix HoverCard dependency and wrap tabs with HoverCard for

* feat(db): move download history schema into migrate + drizzle config

* feat(subscriptions): add item queueing UI and IPC handler

* feat(downloads): normalize saved filenames, improve candidates and UI display
2025-11-15 12:29:31 +08:00
Nexmoe
f1fd40176f feat(ui): make sidebar a draggable title bar and opt out buttons from drag region (#26) 2025-11-15 12:06:59 +08:00
4 changed files with 38 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "vidbee",
"version": "0.3.5",
"version": "1.0.0",
"description": "A modern Electron application for downloading videos and audios",
"main": "./out/main/index.js",
"author": "VidBee",

View File

@@ -448,4 +448,4 @@
"internal": {
"indexes": {}
}
}
}

View File

@@ -27,16 +27,42 @@ import {
} from '../../store/downloads'
import { settingsAtom } from '../../store/settings'
const normalizeSavedFileName = (fileName?: string): string | undefined => {
if (!fileName) {
return undefined
}
const trimmed = fileName.trim()
if (!trimmed) {
return undefined
}
return trimmed.replace(/\.f\d+(?=\.[^.]+$)/i, '')
}
const generateFilePathCandidates = (
downloadPath: string,
title: string,
format: string,
savedFileName?: string
): string[] => {
const candidateFileNames = savedFileName
? [savedFileName]
: [`${title} via VidBee.${format}`, `${title}.${format}`]
const normalizedDownloadPath = downloadPath.replace(/\\/g, '/')
const safeTitle = title.trim() || 'Unknown'
const savedNameCandidates: string[] = []
const trimmedSavedFileName = savedFileName?.trim()
if (trimmedSavedFileName) {
const normalized = normalizeSavedFileName(trimmedSavedFileName)
if (normalized) {
savedNameCandidates.push(normalized)
}
if (!normalized || normalized !== trimmedSavedFileName) {
savedNameCandidates.push(trimmedSavedFileName)
}
}
const candidateFileNames =
savedNameCandidates.length > 0
? savedNameCandidates
: [`${safeTitle} via VidBee.${format}`, `${safeTitle}.${format}`]
return Array.from(
new Set(candidateFileNames.map((fileName) => `${normalizedDownloadPath}/${fileName}`))
)
@@ -56,10 +82,10 @@ const tryFileOperation = async (
}
const getSavedFileExtension = (fileName?: string): string | undefined => {
if (!fileName) {
const normalized = normalizeSavedFileName(fileName)
if (!normalized) {
return undefined
}
const normalized = fileName.trim()
if (!normalized.includes('.')) {
return undefined
}
@@ -182,6 +208,7 @@ export function DownloadItem({ download }: DownloadItemProps) {
? actionsContainerBaseClass
: `${actionsContainerBaseClass} sm:opacity-0 sm:group-hover:opacity-100`
const resolvedExtension = resolveDownloadExtension(download)
const normalizedSavedFileName = normalizeSavedFileName(download.savedFileName)
// Track if the file exists
const [fileExists, setFileExists] = useState(false)
@@ -452,10 +479,10 @@ export function DownloadItem({ download }: DownloadItemProps) {
})
}
if (download.savedFileName) {
if (normalizedSavedFileName || download.savedFileName) {
metadataDetails.push({
label: t('download.metadata.savedFile'),
value: download.savedFileName
value: normalizedSavedFileName ?? download.savedFileName
})
}

View File

@@ -1,4 +1,3 @@
import { Badge } from '@renderer/components/ui/badge'
import { Button } from '@renderer/components/ui/button'
import {
Dialog,
@@ -14,7 +13,7 @@ import { Switch } from '@renderer/components/ui/switch'
import { ipcServices } from '@renderer/lib/ipc'
import { settingsAtom } from '@renderer/store/settings'
import { resolveFeedAtom } from '@renderer/store/subscriptions'
import type { SubscriptionResolvedFeed, SubscriptionRule } from '@shared/types'
import type { SubscriptionRule } from '@shared/types'
import { useAtom, useSetAtom } from 'jotai'
import { ChevronRight } from 'lucide-react'
import { useEffect, useId, useRef, useState } from 'react'
@@ -67,7 +66,6 @@ export function SubscriptionFormDialog({
const [namingTemplate, setNamingTemplate] = useState('')
// Feed detection state
const [detectedFeed, setDetectedFeed] = useState<SubscriptionResolvedFeed | null>(null)
const [detectingFeed, setDetectingFeed] = useState(false)
const detectTimeout = useRef<NodeJS.Timeout | null>(null)
@@ -96,7 +94,6 @@ export function SubscriptionFormDialog({
setDownloadDirectory(settings.downloadPath)
setNamingTemplate(settings.subscriptionFilenameTemplate)
}
setDetectedFeed(null)
}, [
open,
mode,
@@ -137,13 +134,11 @@ export function SubscriptionFormDialog({
// Feed detection logic
useEffect(() => {
if (!url.trim()) {
setDetectedFeed(null)
return
}
// In edit mode, don't detect if URL hasn't changed
if (mode === 'edit' && subscription && url.trim() === subscription.feedUrl) {
setDetectedFeed(null)
return
}
@@ -154,11 +149,9 @@ export function SubscriptionFormDialog({
detectTimeout.current = setTimeout(async () => {
setDetectingFeed(true)
try {
const result = await resolveFeed(url.trim())
setDetectedFeed(result)
await resolveFeed(url.trim())
} catch (error) {
console.error('Failed to resolve feed:', error)
setDetectedFeed(null)
} finally {
setDetectingFeed(false)
}
@@ -252,14 +245,6 @@ export function SubscriptionFormDialog({
placeholder={t('subscriptions.placeholders.url')}
onChange={(event) => setUrl(event.target.value)}
/>
{detectedFeed && (
<Badge variant="outline" className="w-fit text-xs">
{t('subscriptions.detectedFeed', {
platform: detectedFeed.platform,
feed: detectedFeed.feedUrl
})}
</Badge>
)}
{detectingFeed && (
<p className="text-xs text-muted-foreground">{t('subscriptions.detecting')}</p>
)}