Compare commits

...

6 Commits

Author SHA1 Message Date
Nexmoe
877f088136 chore: release v1.1.8 2026-01-12 10:14:49 +08:00
Nexmoe
5ee08a3e62 chore(build): add macOS signing secrets and validation
- Introduced new secrets for macOS signing in the build workflow.
- Added validation to check for the presence of required secrets before proceeding with code signing setup.
2026-01-12 10:13:58 +08:00
Nexmoe
137a07cf4e chore(build): enhance macOS signing and notarization process
- Updated electron-builder configuration to enable hardened runtime and notarization for macOS builds.
- Added a new GitHub Actions step to set up macOS signing with necessary certificates and keys.
2026-01-12 09:58:44 +08:00
Nexmoe
21a5adbdf2 fix(update): localize update toast (#82) 2026-01-11 16:56:34 +08:00
Nexmoe
81f52f86e5 fix(about): hide check updates when available (#80) 2026-01-11 16:34:48 +08:00
Nexmoe
c8147cafe3 fix(download): inline progress details (#81) 2026-01-11 16:34:40 +08:00
8 changed files with 119 additions and 58 deletions

View File

@@ -8,6 +8,17 @@ on:
type: boolean type: boolean
default: false default: false
description: 'Whether to upload build artifacts' description: 'Whether to upload build artifacts'
secrets:
MAC_CERT_P12_BASE64:
required: false
MAC_CERT_P12_PASSWORD:
required: false
APPLE_API_KEY_ID:
required: false
APPLE_API_ISSUER:
required: false
APPLE_API_KEY_P8_BASE64:
required: false
jobs: jobs:
build: build:
@@ -127,6 +138,46 @@ jobs:
- name: Lint and format check - name: Lint and format check
run: pnpm run check && pnpm run typecheck run: pnpm run check && pnpm run typecheck
- name: Setup macOS signing
if: matrix.platform == 'macos'
shell: bash
env:
MAC_CERT_P12_BASE64: ${{ secrets.MAC_CERT_P12_BASE64 }}
MAC_CERT_P12_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
set -euo pipefail
# Check if all required secrets are present
if [[ -z "$MAC_CERT_P12_BASE64" ]] || [[ -z "$MAC_CERT_P12_PASSWORD" ]] || \
[[ -z "$APPLE_API_KEY_ID" ]] || [[ -z "$APPLE_API_ISSUER" ]] || \
[[ -z "$APPLE_API_KEY_P8_BASE64" ]]; then
echo "::notice::macOS signing secrets not available, skipping code signing setup"
exit 0
fi
CERT_PATH="$RUNNER_TEMP/mac_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
echo "$MAC_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
echo "$APPLE_API_KEY_P8_BASE64" | base64 --decode > "$API_KEY_PATH"
security create-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MAC_CERT_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
echo "CSC_KEY_PASSWORD=$MAC_CERT_P12_PASSWORD" >> "$GITHUB_ENV"
echo "APPLE_API_KEY=$API_KEY_PATH" >> "$GITHUB_ENV"
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV"
- name: Build application - name: Build application
run: ${{ matrix.build_script }} run: ${{ matrix.build_script }}

View File

@@ -10,6 +10,7 @@ jobs:
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
with: with:
upload_artifacts: true upload_artifacts: true
secrets: inherit
release: release:
needs: [build] needs: [build]

View File

@@ -28,9 +28,10 @@ nsis:
uninstallDisplayName: ${productName} uninstallDisplayName: ${productName}
createDesktopShortcut: always createDesktopShortcut: always
mac: mac:
identity: null hardenedRuntime: true
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
notarize: false notarize: true
artifactName: ${name}-${version}-${arch}.${ext} artifactName: ${name}-${version}-${arch}.${ext}
target: target:
- target: zip - target: zip

View File

@@ -1,6 +1,6 @@
{ {
"name": "vidbee", "name": "vidbee",
"version": "1.1.7", "version": "1.1.8",
"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",

View File

@@ -364,12 +364,6 @@ function initAutoUpdater(): void {
autoUpdater.on('update-downloaded', (info) => { autoUpdater.on('update-downloaded', (info) => {
log.info('Update downloaded:', info.version) log.info('Update downloaded:', info.version)
mainWindow?.webContents.send('update:downloaded', info) mainWindow?.webContents.send('update:downloaded', info)
if (mainWindow) {
mainWindow.webContents.send('update:show-notification', {
version: info.version
})
}
}) })
log.info('Auto-updater initialized successfully') log.info('Auto-updater initialized successfully')

View File

@@ -55,7 +55,7 @@ function AppContent() {
const loadSettings = useSetAtom(loadSettingsAtom) const loadSettings = useSetAtom(loadSettingsAtom)
const setUpdateReady = useSetAtom(updateReadyAtom) const setUpdateReady = useSetAtom(updateReadyAtom)
const setUpdateAvailable = useSetAtom(updateAvailableAtom) const setUpdateAvailable = useSetAtom(updateAvailableAtom)
const { t } = useTranslation() const { i18n } = 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 navigate = useNavigate()
@@ -197,14 +197,27 @@ function AppContent() {
available: true, available: true,
version: info.version version: info.version
}) })
const versionLabel = info?.version ?? ''
const downloadedMessage = versionLabel
? i18n.t('about.notifications.updateDownloadedVersion', { version: versionLabel })
: i18n.t('about.notifications.updateDownloaded')
toast.info(downloadedMessage, {
action: {
label: i18n.t('about.notifications.restartNowAction'),
onClick: () => {
void ipcServices.update.quitAndInstall()
}
}
})
} }
const handleUpdateError = (rawMessage: unknown) => { const handleUpdateError = (rawMessage: unknown) => {
const message = typeof rawMessage === 'string' ? rawMessage : '' const message = typeof rawMessage === 'string' ? rawMessage : ''
resetDownloadState() resetDownloadState()
const errorMessage = message || t('about.notifications.unknownErrorFallback') const errorMessage = message || i18n.t('about.notifications.unknownErrorFallback')
toast.error(t('about.notifications.updateError', { error: errorMessage })) toast.error(i18n.t('about.notifications.updateError', { error: errorMessage }))
} }
const handleDownloadProgress = (rawProgress: unknown) => { const handleDownloadProgress = (rawProgress: unknown) => {
@@ -214,39 +227,20 @@ function AppContent() {
} }
} }
const handleUpdateNotification = (rawPayload: unknown) => {
const payload = (rawPayload ?? {}) as { version?: string }
const versionLabel = payload?.version ?? ''
const downloadedMessage = versionLabel
? t('about.notifications.updateDownloadedVersion', { version: versionLabel })
: t('about.notifications.updateDownloaded')
toast.info(downloadedMessage, {
action: {
label: t('about.notifications.restartNowAction'),
onClick: () => {
void ipcServices.update.quitAndInstall()
}
}
})
}
// Only listen to update events that should be shown globally // Only listen to update events that should be shown globally
// update:available shows a visual indicator in the sidebar // update:available shows a visual indicator in the sidebar
ipcEvents.on('update:available', handleUpdateAvailable) ipcEvents.on('update:available', handleUpdateAvailable)
ipcEvents.on('update:downloaded', handleUpdateDownloaded) ipcEvents.on('update:downloaded', handleUpdateDownloaded)
ipcEvents.on('update:error', handleUpdateError) ipcEvents.on('update:error', handleUpdateError)
ipcEvents.on('update:download-progress', handleDownloadProgress) ipcEvents.on('update:download-progress', handleDownloadProgress)
ipcEvents.on('update:show-notification', handleUpdateNotification)
return () => { return () => {
ipcEvents.removeListener('update:available', handleUpdateAvailable) ipcEvents.removeListener('update:available', handleUpdateAvailable)
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded) ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
ipcEvents.removeListener('update:error', handleUpdateError) ipcEvents.removeListener('update:error', handleUpdateError)
ipcEvents.removeListener('update:download-progress', handleDownloadProgress) ipcEvents.removeListener('update:download-progress', handleDownloadProgress)
ipcEvents.removeListener('update:show-notification', handleUpdateNotification)
} }
}, [setUpdateAvailable, setUpdateReady, t]) }, [i18n, setUpdateAvailable, setUpdateReady])
return ( return (
<div className="flex flex-row h-screen"> <div className="flex flex-row h-screen">

View File

@@ -400,6 +400,10 @@ export function DownloadItem({ download, isSelected = false, onToggleSelect }: D
const statusIcon = getStatusIcon() const statusIcon = getStatusIcon()
const statusText = getStatusText() const statusText = getStatusText()
const progressInfo = download.progress
const showInlineProgress = Boolean(
progressInfo && download.status !== 'completed' && download.status !== 'error'
)
const sourceDisplay = const sourceDisplay =
download.uploader && download.channel && download.uploader !== download.channel download.uploader && download.channel && download.uploader !== download.channel
? `${download.uploader}${download.channel}` ? `${download.uploader}${download.channel}`
@@ -725,6 +729,24 @@ export function DownloadItem({ download, isSelected = false, onToggleSelect }: D
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
{showInlineProgress && (
<div className="flex items-center gap-2 min-w-0">
<span className="font-medium shrink-0">
{(progressInfo?.percent ?? 0).toFixed(1)}%
</span>
{progressInfo?.downloaded && progressInfo?.total && (
<span className="truncate max-w-[120px]">
{progressInfo.downloaded} / {progressInfo.total}
</span>
)}
{progressInfo?.currentSpeed && (
<span className="truncate max-w-[80px]">{progressInfo.currentSpeed}</span>
)}
{progressInfo?.eta && (
<span className="truncate max-w-[80px]">ETA: {progressInfo.eta}</span>
)}
</div>
)}
{/* Timestamp */} {/* Timestamp */}
{timestamp && ( {timestamp && (
<span className="truncate shrink-0">{formatDateShort(timestamp)}</span> <span className="truncate shrink-0">{formatDateShort(timestamp)}</span>
@@ -898,26 +920,8 @@ export function DownloadItem({ download, isSelected = false, onToggleSelect }: D
{/* Progress */} {/* Progress */}
{download.progress && download.status !== 'completed' && download.status !== 'error' && ( {download.progress && download.status !== 'completed' && download.status !== 'error' && (
<div className="space-y-1 bg-background/60 w-full overflow-hidden"> <div className="bg-background/60 w-full overflow-hidden">
<Progress value={download.progress.percent} className="h-1 w-full" /> <Progress value={download.progress.percent} className="h-1 w-full" />
<div className="flex flex-wrap items-center justify-between gap-2 text-[11px] text-muted-foreground w-full">
<span className="font-medium shrink-0">
{download.progress.percent.toFixed(1)}%
</span>
<div className="flex flex-wrap items-center gap-2 min-w-0 flex-1">
{download.progress.downloaded && download.progress.total && (
<span className="truncate max-w-[100px]">
{download.progress.downloaded} / {download.progress.total}
</span>
)}
{download.progress.currentSpeed && (
<span className="truncate max-w-[80px]">{download.progress.currentSpeed}</span>
)}
{download.progress.eta && (
<span className="truncate max-w-[80px]">ETA: {download.progress.eta}</span>
)}
</div>
</div>
</div> </div>
)} )}

View File

@@ -39,9 +39,10 @@ type LatestVersionState =
| null | null
export function About() { export function About() {
const { t } = useTranslation() const { t, i18n } = useTranslation()
const [settings, _setSettings] = useAtom(settingsAtom) const [settings, _setSettings] = useAtom(settingsAtom)
const [updateReady] = useAtom(updateReadyAtom) const [updateReady] = useAtom(updateReadyAtom)
const [updateAvailableState] = useAtom(updateAvailableAtom)
const setUpdateAvailable = useSetAtom(updateAvailableAtom) const setUpdateAvailable = useSetAtom(updateAvailableAtom)
const [appVersion, setAppVersion] = useState<string>('—') const [appVersion, setAppVersion] = useState<string>('—')
const [latestVersionState, setLatestVersionState] = useState<LatestVersionState>(null) const [latestVersionState, setLatestVersionState] = useState<LatestVersionState>(null)
@@ -70,6 +71,17 @@ export function About() {
} }
}, []) }, [])
useEffect(() => {
if (!updateAvailableState.available) {
return
}
setLatestVersionState({
status: 'available',
version: updateAvailableState.version ?? ''
})
}, [updateAvailableState.available, updateAvailableState.version])
// Listen for update events only in About page // Listen for update events only in About page
useEffect(() => { useEffect(() => {
if (!window?.api) { if (!window?.api) {
@@ -81,7 +93,7 @@ export function About() {
const versionLabel = info.version ?? '' const versionLabel = info.version ?? ''
// Update will be downloaded automatically because autoDownload is enabled in main process // Update will be downloaded automatically because autoDownload is enabled in main process
toast.success(t('about.notifications.updateAvailable', { version: versionLabel })) toast.success(i18n.t('about.notifications.updateAvailable', { version: versionLabel }))
setLatestVersionState({ setLatestVersionState({
status: 'available', status: 'available',
version: versionLabel version: versionLabel
@@ -115,7 +127,7 @@ export function About() {
ipcEvents.removeListener('update:download-progress', handleUpdateDownloadProgress) ipcEvents.removeListener('update:download-progress', handleUpdateDownloadProgress)
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded) ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
} }
}, [setUpdateAvailable, t]) }, [i18n, setUpdateAvailable])
const handleSettingChange = async ( const handleSettingChange = async (
key: keyof typeof settings, key: keyof typeof settings,
@@ -264,6 +276,8 @@ export function About() {
? 'text-destructive' ? 'text-destructive'
: 'text-muted-foreground' : 'text-muted-foreground'
const latestVersionStatusText = latestVersionStatusKey ? t(latestVersionStatusKey) : null const latestVersionStatusText = latestVersionStatusKey ? t(latestVersionStatusKey) : null
const shouldShowCheckUpdates =
!updateAvailableState.available && latestVersionState?.status !== 'available'
const handleXFeedback = useCallback(() => { const handleXFeedback = useCallback(() => {
const versionText = appVersion !== '—' ? ` VidBee v${appVersion}` : ' VidBee' const versionText = appVersion !== '—' ? ` VidBee v${appVersion}` : ' VidBee'
@@ -379,10 +393,12 @@ export function About() {
{t('about.actions.goToDownload')} {t('about.actions.goToDownload')}
</Button> </Button>
) : null} ) : null}
<Button onClick={handleCheckForUpdates} size="sm" className="gap-2"> {shouldShowCheckUpdates ? (
<RefreshCw className="h-3.5 w-3.5" /> <Button onClick={handleCheckForUpdates} size="sm" className="gap-2">
{t('about.actions.checkUpdates')} <RefreshCw className="h-3.5 w-3.5" />
</Button> {t('about.actions.checkUpdates')}
</Button>
) : null}
</div> </div>
</div> </div>
<p className="text-sm text-muted-foreground">{t('about.description')}</p> <p className="text-sm text-muted-foreground">{t('about.description')}</p>