feat(extension): refine error UI and branding (#89)
This commit is contained in:
84
.github/workflows/extension-publish.yml
vendored
Normal file
84
.github/workflows/extension-publish.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Publish Extension
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- extension/package.json
|
||||
|
||||
jobs:
|
||||
detect-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.version_check.outputs.changed }}
|
||||
current_version: ${{ steps.version_check.outputs.current_version }}
|
||||
previous_version: ${{ steps.version_check.outputs.previous_version }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if extension version changed
|
||||
id: version_check
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
before_sha="${{ github.event.before }}"
|
||||
current_version=$(node -e "const fs = require('fs'); const v = JSON.parse(fs.readFileSync('extension/package.json', 'utf8')).version; process.stdout.write(v);")
|
||||
previous_version=""
|
||||
if git cat-file -e "${before_sha}:extension/package.json" 2>/dev/null; then
|
||||
previous_version=$(git show "${before_sha}:extension/package.json" | node -e "let data=''; process.stdin.on('data', d => data += d); process.stdin.on('end', () => { const v = JSON.parse(data).version; process.stdout.write(v); });")
|
||||
fi
|
||||
|
||||
echo "current_version=${current_version}" >> "$GITHUB_OUTPUT"
|
||||
echo "previous_version=${previous_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [[ -n "${previous_version}" && "${current_version}" == "${previous_version}" ]]; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
submit:
|
||||
needs: detect-version
|
||||
if: needs.detect-version.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: extension
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: extension/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
pnpm zip
|
||||
|
||||
- name: Submit to stores
|
||||
run: |
|
||||
pnpm wxt submit \
|
||||
--chrome-zip .output/*-chrome.zip
|
||||
env:
|
||||
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
CHROME_PUBLISH_TARGET: "default"
|
||||
CHROME_SKIP_SUBMIT_REVIEW: false
|
||||
3
extension/.gitignore
vendored
3
extension/.gitignore
vendored
@@ -24,3 +24,6 @@ web-ext.config.ts
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
.env.*
|
||||
|
||||
@@ -1,3 +1,54 @@
|
||||
# WXT + React
|
||||
# VidBee Video Downloader Extension
|
||||
|
||||
This template should help get you started developing with React in WXT.
|
||||
VidBee Video Downloader is a lightweight browser companion for the VidBee desktop app. It detects the video on your current tab, shows the available formats, and hands the URL to VidBee so the download happens in the desktop app instead of your browser.
|
||||
|
||||
## Why install it?
|
||||
|
||||
- **One-click handoff:** Send the current video page to VidBee without copy-pasting links.
|
||||
- **See formats before downloading:** Preview resolutions, file sizes, and audio-only options in the popup.
|
||||
- **More reliable downloads:** VidBee handles large files and multi-format downloads better than most browsers.
|
||||
- **Works on 1,000+ sites:** The extension uses the same site support as the VidBee app.
|
||||
|
||||
## What it does
|
||||
|
||||
1. Reads the active tab URL when you open the popup.
|
||||
2. Asks the VidBee desktop app (running locally) to analyze the video.
|
||||
3. Displays the formats it finds and lets you open VidBee to download.
|
||||
|
||||
## Requirements
|
||||
|
||||
- VidBee desktop app installed.
|
||||
- VidBee running while you use the extension.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Open a supported video page.
|
||||
2. Click the VidBee extension icon.
|
||||
3. Review available formats.
|
||||
4. Click **Download with VidBee** to start the download in the desktop app.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Build or package:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm zip
|
||||
```
|
||||
|
||||
Firefox builds:
|
||||
|
||||
```bash
|
||||
pnpm dev:firefox
|
||||
pnpm build:firefox
|
||||
pnpm zip:firefox
|
||||
```
|
||||
|
||||
## Notes on privacy
|
||||
|
||||
The extension only sends the current tab URL to the local VidBee app on `127.0.0.1` and stores temporary results in browser storage for faster reloads.
|
||||
|
||||
@@ -208,16 +208,42 @@ h1 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.troubleshoot-box {
|
||||
background: var(--border);
|
||||
.error-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 12px;
|
||||
color: var(--fg-secondary);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.troubleshoot-title {
|
||||
.action-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--fg-secondary);
|
||||
@@ -226,31 +252,31 @@ h1 {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.troubleshoot-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.action-text {
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
.secondary-button {
|
||||
border: 1px solid var(--fg);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: var(--fg-secondary);
|
||||
.secondary-button:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -265,14 +265,49 @@ function App() {
|
||||
return result
|
||||
}, [groupedFormats.video])
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const clientLaunchDelayMs = 2000
|
||||
|
||||
const openClientApp = async () => {
|
||||
window.location.href = 'vidbee://'
|
||||
await wait(clientLaunchDelayMs)
|
||||
}
|
||||
|
||||
const handleOpenClient = () => {
|
||||
if (!currentUrl) return
|
||||
const deepLink = `vidbee://download?url=${encodeURIComponent(currentUrl)}`
|
||||
window.location.href = deepLink
|
||||
}
|
||||
|
||||
const handleOpenClientAndRetry = async () => {
|
||||
await openClientApp()
|
||||
setRetryTrigger((count) => count + 1)
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
setRetryTrigger((count) => count + 1)
|
||||
}
|
||||
|
||||
const isInvalidPageError = error === 'Please open a valid video page first.'
|
||||
const isClientConnectionError = Boolean(error?.includes('Client connection failed'))
|
||||
const errorTitle = isInvalidPageError
|
||||
? 'Open a video page'
|
||||
: isClientConnectionError
|
||||
? 'Connect the VidBee app'
|
||||
: 'Something went wrong'
|
||||
const errorDescription = isInvalidPageError
|
||||
? 'Navigate to a supported video page, then try again.'
|
||||
: isClientConnectionError
|
||||
? 'The extension needs the VidBee desktop app to be running.'
|
||||
: 'Try again in a moment.'
|
||||
|
||||
const renderStatus = () => {
|
||||
// if (loading) return null
|
||||
if (loading)
|
||||
return (
|
||||
<span className="status-indicator">
|
||||
<div className="status-dot loading" /> Working
|
||||
</span>
|
||||
)
|
||||
if (error)
|
||||
return (
|
||||
<span className="status-indicator">
|
||||
@@ -307,31 +342,54 @@ function App() {
|
||||
|
||||
{!loading && error && (
|
||||
<div className="error-container">
|
||||
<div className="error-banner">{error}</div>
|
||||
<div className="troubleshoot-box">
|
||||
<p className="troubleshoot-title">Having trouble?</p>
|
||||
<div className="troubleshoot-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="link-button"
|
||||
onClick={() => {
|
||||
window.location.href = 'vidbee://'
|
||||
setTimeout(() => setRetryTrigger((c) => c + 1), 100)
|
||||
}}
|
||||
>
|
||||
Open Client
|
||||
</button>
|
||||
<span className="divider">•</span>
|
||||
<a
|
||||
href="https://vidbee.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link-button"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<div className="error-header">
|
||||
<h2 className="error-title">{errorTitle}</h2>
|
||||
<p className="error-description">{errorDescription}</p>
|
||||
</div>
|
||||
<div className="error-banner">{error}</div>
|
||||
{isClientConnectionError ? (
|
||||
<div className="action-grid">
|
||||
<div className="action-card">
|
||||
<p className="action-title">Client installed</p>
|
||||
<p className="action-text">
|
||||
Start VidBee and keep it running, then we will retry automatically.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={handleOpenClientAndRetry}
|
||||
>
|
||||
Open Client
|
||||
</button>
|
||||
</div>
|
||||
<div className="action-card">
|
||||
<p className="action-title">Need the app?</p>
|
||||
<p className="action-text">
|
||||
Download VidBee once, install it, then come back here to try again.
|
||||
</p>
|
||||
<a
|
||||
href="https://vidbee.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button"
|
||||
>
|
||||
Download VidBee
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="action-card">
|
||||
<p className="action-title">Try again</p>
|
||||
<p className="action-text">
|
||||
{isInvalidPageError
|
||||
? 'Open a supported video page, then retry.'
|
||||
: 'Retry after a moment.'}
|
||||
</p>
|
||||
<button type="button" className="secondary-button" onClick={handleRetry}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "wxt-react-starter",
|
||||
"name": "vidbee-extension",
|
||||
"description": "manifest.json description",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"extensionName": {
|
||||
"message": "VidBee Video Downloader"
|
||||
},
|
||||
"extensionDescription": {
|
||||
"message": "Download videos from over 1,000 websites with VidBee."
|
||||
},
|
||||
"downloadWithVidBee": {
|
||||
"message": "Download with VidBee"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ import { defineConfig } from 'wxt'
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-react'],
|
||||
manifest: {
|
||||
name: '__MSG_extensionName__',
|
||||
description: '__MSG_extensionDescription__',
|
||||
default_locale: 'en',
|
||||
host_permissions: ['http://127.0.0.1/*'],
|
||||
permissions: ['activeTab', 'storage']
|
||||
|
||||
Reference in New Issue
Block a user