mirror of
https://github.com/zen-browser/www.git
synced 2025-07-10 02:05:31 +02:00
feat(checksum): add checksum support to download components and update workflow
- Enhanced ButtonCard component to accept and display SHA-256 checksums. - Updated PlatformDownload and release-data to include checksum information for each release. - Modified DownloadScript to apply twilight mode to new checksum buttons. - Adjusted download page to retrieve and display checksums dynamically.
This commit is contained in:
parent
59974742e9
commit
3816206f6b
7 changed files with 234 additions and 76 deletions
2
.github/workflows/prbuildcheck.yml
vendored
2
.github/workflows/prbuildcheck.yml
vendored
|
@ -23,3 +23,5 @@ jobs:
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
@ -3,44 +3,151 @@ interface Props {
|
||||||
label: string
|
label: string
|
||||||
href: string
|
href: string
|
||||||
variant?: string
|
variant?: string
|
||||||
|
checksum?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { label, href } = Astro.props
|
const { label, href, checksum } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<a
|
<div class="relative flex flex-col">
|
||||||
href={href}
|
<a
|
||||||
class="download-link group relative flex items-center justify-between overflow-hidden rounded-2xl border border-subtle p-4 transition-all duration-200 hover:border-coral hover:shadow-sm data-[twilight='true']:hover:border-zen-blue dark:hover:shadow-md"
|
href={href}
|
||||||
rel="noopener noreferrer"
|
class="download-link group flex flex-1 items-center justify-between rounded-2xl border border-subtle p-4 transition-all duration-200 hover:border-coral hover:shadow-sm data-[twilight='true']:hover:border-zen-blue dark:hover:shadow-md"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
<div>
|
tabindex="0"
|
||||||
<p class="text-lg font-medium">{label}</p>
|
>
|
||||||
</div>
|
<div class="flex items-center gap-2">
|
||||||
<div class="ml-4 flex flex-col items-end">
|
<p class="text-lg font-medium">{label}</p>
|
||||||
<div class="flex items-center">
|
{
|
||||||
<span
|
checksum && (
|
||||||
class="release-type-tag mr-2 rounded-full bg-coral/10 px-2 py-1 text-xs font-medium text-coral transition-colors duration-200 group-hover:bg-coral/20 data-[twilight='true']:bg-zen-blue/10 data-[twilight='true']:text-zen-blue data-[twilight='true']:group-hover:bg-zen-blue/20"
|
<span class="group/checksum relative hidden items-center md:flex">
|
||||||
>
|
<button
|
||||||
Beta
|
type="button"
|
||||||
</span>
|
class="checksum-icon-btn text-muted-foreground flex items-center justify-center rounded-full p-1 hover:text-coral focus:outline-none focus:ring-2 focus:ring-coral data-[twilight='true']:hover:text-zen-blue data-[twilight='true']:focus:ring-zen-blue"
|
||||||
<div
|
aria-label="Show SHA-256 checksum"
|
||||||
class="download-arrow-icon text-muted-foreground rounded-xl border border-subtle p-2 transition-colors duration-200 group-hover:border-coral group-hover:text-coral data-[twilight='true']:group-hover:border-zen-blue data-[twilight='true']:group-hover:text-zen-blue"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="18"
|
||||||
height="16"
|
height="18"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
stroke-width="2"
|
||||||
stroke-width="2"
|
stroke-linecap="round"
|
||||||
stroke-linecap="round"
|
stroke-linejoin="round"
|
||||||
stroke-linejoin="round"
|
viewBox="0 0 24 24"
|
||||||
class="lucide lucide-arrow-up-right"
|
>
|
||||||
|
<path d="M12 3l8 4v5c0 5.25-3.5 9.74-8 11-4.5-1.26-8-5.75-8-11V7l8-4z" />
|
||||||
|
<path d="M9 12l2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="absolute -top-10 left-1/2 z-50 hidden min-w-[120px] -translate-x-1/2 select-none whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 shadow group-focus-within/checksum:hidden group-hover/checksum:flex group-focus-within/checksum:group-hover/checksum:hidden dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
|
||||||
|
Show SHA-256
|
||||||
|
</span>
|
||||||
|
<span class="checksum-tooltip popover absolute -left-14 -top-12 z-50 hidden min-w-[220px] items-center gap-2 whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 shadow group-focus-within/checksum:flex dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
|
||||||
|
<span class="flex-1 truncate font-mono text-xs">{checksum}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="copy-btn rounded bg-coral px-2 py-1 text-xs text-white hover:bg-coral/80 data-[twilight='true']:bg-zen-blue data-[twilight='true']:hover:bg-zen-blue/80"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="release-type-tag rounded-full bg-coral/10 px-2 py-1 text-xs font-medium text-coral transition-colors duration-200 group-hover:bg-coral/20 data-[twilight='true']:bg-zen-blue/10 data-[twilight='true']:text-zen-blue data-[twilight='true']:group-hover:bg-zen-blue/20"
|
||||||
>
|
>
|
||||||
<path d="M7 17 17 7"></path>
|
Beta
|
||||||
<path d="M7 7h10v10"></path>
|
</span>
|
||||||
</svg>
|
<div
|
||||||
|
class="download-arrow-icon text-muted-foreground rounded-xl border border-subtle p-2 transition-colors duration-200 group-hover:border-coral group-hover:text-coral data-[twilight='true']:group-hover:border-zen-blue data-[twilight='true']:group-hover:text-zen-blue"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-arrow-up-right"
|
||||||
|
>
|
||||||
|
<path d="M7 17 17 7"></path>
|
||||||
|
<path d="M7 7h10v10"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.checksum-expand {
|
||||||
|
transition: all 0.2s;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.checksum-icon-btn {
|
||||||
|
transition:
|
||||||
|
color 0.15s,
|
||||||
|
background 0.15s;
|
||||||
|
}
|
||||||
|
.checksum-tooltip {
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const checksumButtons = document.querySelectorAll(
|
||||||
|
'.checksum-icon-btn',
|
||||||
|
) as NodeListOf<HTMLButtonElement>
|
||||||
|
const checksumTooltips = document.querySelectorAll(
|
||||||
|
'.checksum-tooltip',
|
||||||
|
) as NodeListOf<HTMLDivElement>
|
||||||
|
const copyButtons = document.querySelectorAll(
|
||||||
|
'.copy-btn',
|
||||||
|
) as NodeListOf<HTMLButtonElement>
|
||||||
|
|
||||||
|
function stopEvent(e: Event) {
|
||||||
|
e.preventDefault?.()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyChecksum(e: Event, checksum: string) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
navigator.clipboard.writeText(checksum)
|
||||||
|
const btn = e.currentTarget as HTMLButtonElement
|
||||||
|
const original = btn.innerText
|
||||||
|
btn.innerText = 'Copied!'
|
||||||
|
setTimeout(() => (btn.innerText = original), 1200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach listeners after DOM is ready
|
||||||
|
checksumButtons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', stopEvent)
|
||||||
|
})
|
||||||
|
checksumTooltips.forEach((tooltip) => {
|
||||||
|
tooltip.addEventListener('mousedown', stopEvent)
|
||||||
|
tooltip.addEventListener('click', stopEvent)
|
||||||
|
})
|
||||||
|
copyButtons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', (e) =>
|
||||||
|
copyChecksum(
|
||||||
|
e,
|
||||||
|
(
|
||||||
|
btn
|
||||||
|
.closest('.checksum-tooltip')
|
||||||
|
?.querySelector('.font-mono') as HTMLSpanElement
|
||||||
|
)?.innerText,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
btn.addEventListener('mousedown', stopEvent)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
// Apply twilight mode to all relevant elements
|
// Apply twilight mode to all relevant elements
|
||||||
const coralElements = document.querySelectorAll(
|
const coralElements = document.querySelectorAll(
|
||||||
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon',
|
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn',
|
||||||
)
|
)
|
||||||
coralElements.forEach((element) => {
|
coralElements.forEach((element) => {
|
||||||
element.setAttribute('data-twilight', 'true')
|
element.setAttribute('data-twilight', 'true')
|
||||||
|
|
|
@ -49,11 +49,13 @@ import DownloadCard from './ButtonCard.astro'
|
||||||
label="x86_64"
|
label="x86_64"
|
||||||
href={releases.x86_64.tarball.link}
|
href={releases.x86_64.tarball.link}
|
||||||
variant="x86_64"
|
variant="x86_64"
|
||||||
|
checksum={releases.x86_64.tarball.checksum}
|
||||||
/>
|
/>
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
label="ARM64"
|
label="ARM64"
|
||||||
href={releases.aarch64.tarball.link}
|
href={releases.aarch64.tarball.link}
|
||||||
variant="aarch64"
|
variant="aarch64"
|
||||||
|
checksum={releases.aarch64.tarball.checksum}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,6 +67,7 @@ import DownloadCard from './ButtonCard.astro'
|
||||||
label={releaseNote.label}
|
label={releaseNote.label}
|
||||||
href={releaseNote.link}
|
href={releaseNote.link}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
checksum={releaseNote.checksum}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,50 +1,61 @@
|
||||||
---
|
---
|
||||||
import { releaseNotes, releaseNotesTwilight } from '../../release-notes'
|
/**
|
||||||
|
* Returns the releases object, injecting checksums dynamically.
|
||||||
export const releases = {
|
* @param checksums Record<string, string> mapping filenames to SHA-256 hashes
|
||||||
macos: {
|
*/
|
||||||
universal: {
|
export function getReleasesWithChecksums(checksums: Record<string, string>) {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
|
return {
|
||||||
label: `Universal`,
|
macos: {
|
||||||
},
|
universal: {
|
||||||
},
|
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
|
||||||
windows: {
|
label: `Universal`,
|
||||||
x86_64: {
|
checksum: checksums['zen.macos-universal.dmg'],
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
|
|
||||||
label: `64-bit (Recommended)`,
|
|
||||||
},
|
|
||||||
arm64: {
|
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
|
|
||||||
label: `ARM64`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
linux: {
|
|
||||||
x86_64: {
|
|
||||||
tarball: {
|
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
|
|
||||||
label: `Tarball x86_64`,
|
|
||||||
},
|
|
||||||
appImage: {
|
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage',
|
|
||||||
label: `AppImage x86_64`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aarch64: {
|
windows: {
|
||||||
tarball: {
|
x86_64: {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
|
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
|
||||||
label: `Tarball aarch64`,
|
label: `64-bit (Recommended)`,
|
||||||
|
checksum: checksums['zen.installer.exe'],
|
||||||
},
|
},
|
||||||
appImage: {
|
arm64: {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage',
|
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
|
||||||
label: `AppImage aarch64`,
|
label: `ARM64`,
|
||||||
|
checksum: checksums['zen.installer-arm64.exe'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flathub: {
|
linux: {
|
||||||
all: {
|
x86_64: {
|
||||||
link: 'https://flathub.org/apps/app.zen_browser.zen',
|
tarball: {
|
||||||
label: `Flathub`,
|
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
|
||||||
|
label: `Tarball x86_64`,
|
||||||
|
checksum: checksums['zen.linux-x86_64.tar.xz'],
|
||||||
|
},
|
||||||
|
appImage: {
|
||||||
|
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-x86_64.AppImage',
|
||||||
|
label: `AppImage x86_64`,
|
||||||
|
checksum: checksums['zen-x86_64.AppImage'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aarch64: {
|
||||||
|
tarball: {
|
||||||
|
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
|
||||||
|
label: `Tarball aarch64`,
|
||||||
|
checksum: checksums['zen.linux-aarch64.tar.xz'],
|
||||||
|
},
|
||||||
|
appImage: {
|
||||||
|
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen-aarch64.AppImage',
|
||||||
|
label: `AppImage aarch64`,
|
||||||
|
checksum: checksums['zen-aarch64.AppImage'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flathub: {
|
||||||
|
all: {
|
||||||
|
link: 'https://flathub.org/apps/app.zen_browser.zen',
|
||||||
|
label: `Flathub`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
import Description from '../components/Description.astro'
|
import Description from '../components/Description.astro'
|
||||||
import Title from '../components/Title.astro'
|
import Title from '../components/Title.astro'
|
||||||
import Layout from '../layouts/Layout.astro'
|
import Layout from '../layouts/Layout.astro'
|
||||||
import { releases } from '../components/download/release-data.astro'
|
import { getReleasesWithChecksums } from '../components/download/release-data.astro'
|
||||||
import PlatformDownload from '../components/download/PlatformDownload.astro'
|
import PlatformDownload from '../components/download/PlatformDownload.astro'
|
||||||
import DownloadScript from '../components/download/DownloadScript.astro'
|
import DownloadScript from '../components/download/DownloadScript.astro'
|
||||||
|
import { getChecksums } from '../utils/githubChecksums'
|
||||||
|
|
||||||
import { library, icon } from '@fortawesome/fontawesome-svg-core'
|
import { library, icon } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -20,6 +21,9 @@ const windowsIcon = icon({ prefix: 'fab', iconName: 'windows' })
|
||||||
const linuxIcon = icon({ prefix: 'fab', iconName: 'linux' })
|
const linuxIcon = icon({ prefix: 'fab', iconName: 'linux' })
|
||||||
const appleIcon = icon({ prefix: 'fab', iconName: 'apple' })
|
const appleIcon = icon({ prefix: 'fab', iconName: 'apple' })
|
||||||
const githubIcon = icon({ prefix: 'fab', iconName: 'github' })
|
const githubIcon = icon({ prefix: 'fab', iconName: 'github' })
|
||||||
|
|
||||||
|
const checksums = await getChecksums()
|
||||||
|
const releases = getReleasesWithChecksums(checksums)
|
||||||
---
|
---
|
||||||
|
|
||||||
<DownloadScript />
|
<DownloadScript />
|
||||||
|
|
31
src/utils/githubChecksums.ts
Normal file
31
src/utils/githubChecksums.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Fetches the latest release notes from GitHub and parses the SHA-256 checksums.
|
||||||
|
* Returns a mapping from filename to checksum.
|
||||||
|
*/
|
||||||
|
export async function getChecksums() {
|
||||||
|
const token = import.meta.env.GITHUB_TOKEN;
|
||||||
|
if (!token) throw new Error('GITHUB_TOKEN is not set in environment variables');
|
||||||
|
|
||||||
|
const res = await fetch('https://api.github.com/repos/zen-browser/desktop/releases/latest', {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.github+json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
|
'User-Agent': 'zen-browser-checksum-fetcher',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch GitHub release: ' + res.statusText);
|
||||||
|
const data = await res.json();
|
||||||
|
const body = data.body as string;
|
||||||
|
|
||||||
|
// Extract the checksum block
|
||||||
|
const match = body.match(/File Checksums \(SHA-256\)[\s\S]*?```([\s\S]*?)```/);
|
||||||
|
const checksums: Record<string, string> = {};
|
||||||
|
if (match && match[1]) {
|
||||||
|
match[1].split('\n').forEach(line => {
|
||||||
|
const [hash, filename] = line.trim().split(/\s+/, 2);
|
||||||
|
if (hash && filename) checksums[filename] = hash;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return checksums;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue