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
default: false
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:
build:
@@ -127,6 +138,46 @@ jobs:
- name: Lint and format check
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
run: ${{ matrix.build_script }}

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ function AppContent() {
const loadSettings = useSetAtom(loadSettingsAtom)
const setUpdateReady = useSetAtom(updateReadyAtom)
const setUpdateAvailable = useSetAtom(updateAvailableAtom)
const { t } = useTranslation()
const { i18n } = useTranslation()
const updateDownloadInProgressRef = useRef(false)
const analyticsScriptRef = useRef<HTMLScriptElement | null>(null)
const navigate = useNavigate()
@@ -197,14 +197,27 @@ function AppContent() {
available: true,
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 message = typeof rawMessage === 'string' ? rawMessage : ''
resetDownloadState()
const errorMessage = message || t('about.notifications.unknownErrorFallback')
toast.error(t('about.notifications.updateError', { error: errorMessage }))
const errorMessage = message || i18n.t('about.notifications.unknownErrorFallback')
toast.error(i18n.t('about.notifications.updateError', { error: errorMessage }))
}
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
// update:available shows a visual indicator in the sidebar
ipcEvents.on('update:available', handleUpdateAvailable)
ipcEvents.on('update:downloaded', handleUpdateDownloaded)
ipcEvents.on('update:error', handleUpdateError)
ipcEvents.on('update:download-progress', handleDownloadProgress)
ipcEvents.on('update:show-notification', handleUpdateNotification)
return () => {
ipcEvents.removeListener('update:available', handleUpdateAvailable)
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
ipcEvents.removeListener('update:error', handleUpdateError)
ipcEvents.removeListener('update:download-progress', handleDownloadProgress)
ipcEvents.removeListener('update:show-notification', handleUpdateNotification)
}
}, [setUpdateAvailable, setUpdateReady, t])
}, [i18n, setUpdateAvailable, setUpdateReady])
return (
<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 statusText = getStatusText()
const progressInfo = download.progress
const showInlineProgress = Boolean(
progressInfo && download.status !== 'completed' && download.status !== 'error'
)
const sourceDisplay =
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>
</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 && (
<span className="truncate shrink-0">{formatDateShort(timestamp)}</span>
@@ -898,26 +920,8 @@ export function DownloadItem({ download, isSelected = false, onToggleSelect }: D
{/* Progress */}
{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" />
<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>
)}

View File

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