mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-24 18:02:41 -04:00
#1652: add a homepage widget: download version suggester
This commit is contained in:
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
paths:
|
||||
- .github/workflows/deploy.yml
|
||||
- docs/**
|
||||
- .vitepress/**
|
||||
- index.md
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
238
.vitepress/theme/DownloadSuggestionWidget.vue
Normal file
238
.vitepress/theme/DownloadSuggestionWidget.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { getDetectOS } from './download-detection.js'
|
||||
|
||||
const PREVIEW_OPTIONS = [
|
||||
{ value: '', label: 'Current system' },
|
||||
{ value: 'windows|x64', label: 'Windows (x64)' },
|
||||
{ value: 'windows|arm64', label: 'Windows (arm64)' },
|
||||
{ value: 'mac|x64', label: 'Mac (x64)' },
|
||||
{ value: 'mac|arm64', label: 'Mac (arm64)' },
|
||||
{ value: 'linux|x64|', label: 'Linux (x64)' },
|
||||
{ value: 'linux|x64|Ubuntu', label: 'Linux Ubuntu (x64)' },
|
||||
{ value: 'linux|x64|Fedora', label: 'Linux Fedora (x64)' },
|
||||
{ value: 'linux|arm64|', label: 'Linux (arm64)' },
|
||||
{ value: 'linux|arm64|Ubuntu', label: 'Linux Ubuntu (arm64)' },
|
||||
{ value: 'linux|arm64|Fedora', label: 'Linux Fedora (arm64)' },
|
||||
]
|
||||
|
||||
const INSTALL_MAC_URL = 'https://getlibation.com/docs/installation/mac'
|
||||
const INSTALL_LINUX_URL = 'https://getlibation.com/docs/installation/linux'
|
||||
|
||||
const status = ref('loading') // 'loading' | 'ready' | 'error'
|
||||
const errorMessage = ref('')
|
||||
const cachedAssets = ref([])
|
||||
const rec = ref(null)
|
||||
const blockHead = ref('')
|
||||
const previewValue = ref('')
|
||||
const installGuideUrl = ref(null)
|
||||
const installGuideLabel = ref('')
|
||||
const d = ref(null)
|
||||
|
||||
function getPreviewOverrides() {
|
||||
if (!previewValue.value) return null
|
||||
const parts = previewValue.value.split('|')
|
||||
const overrides = { os: parts[0], architecture: parts[1] }
|
||||
if (parts.length > 2 && parts[2]) overrides.linuxFlavor = parts[2]
|
||||
return overrides
|
||||
}
|
||||
|
||||
function recommendedForLabel(overrides) {
|
||||
if (!d.value) return 'Recommended for your system'
|
||||
const os = (overrides && overrides.os) || d.value.os
|
||||
const ver = (overrides && overrides.osVersion) || d.value.osVersion
|
||||
const arch = (overrides && overrides.architecture) || d.value.architecture
|
||||
const flavor = (overrides && overrides.linuxFlavor !== undefined) ? overrides.linuxFlavor : d.value.linuxFlavor
|
||||
if (os === 'windows') return 'Recommended for Windows (' + arch + ')'
|
||||
if (os === 'mac') return 'Recommended for Mac ' + (ver ? ver + ' ' : '') + '(' + arch + ')'
|
||||
if (os === 'linux') return 'Recommended for ' + (flavor ? flavor + ' ' : '') + 'Linux (' + arch + ')'
|
||||
return 'Recommended for your system'
|
||||
}
|
||||
|
||||
function updateRecommendation() {
|
||||
if (!cachedAssets.value.length || !d.value) return
|
||||
const overrides = getPreviewOverrides()
|
||||
rec.value = d.value.recommendDownload(cachedAssets.value, overrides)
|
||||
blockHead.value = recommendedForLabel(overrides)
|
||||
const os = (overrides && overrides.os) || d.value.os
|
||||
if (os === 'mac') {
|
||||
installGuideUrl.value = INSTALL_MAC_URL
|
||||
installGuideLabel.value = 'Install on macOS'
|
||||
} else if (os === 'linux') {
|
||||
installGuideUrl.value = INSTALL_LINUX_URL
|
||||
installGuideLabel.value = 'Install on Linux'
|
||||
} else {
|
||||
installGuideUrl.value = null
|
||||
installGuideLabel.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const showReasonWarn = computed(() => {
|
||||
return rec.value?.reason && rec.value.reason !== 'Recommended for your system.'
|
||||
})
|
||||
|
||||
const showAssets = computed(() => {
|
||||
if (!rec.value || !d.value || status.value !== 'ready') return []
|
||||
const libationAssets = cachedAssets.value
|
||||
const overrides = getPreviewOverrides()
|
||||
const os = (overrides && overrides.os) || d.value.os
|
||||
const arch = (overrides && overrides.architecture) || d.value.architecture
|
||||
return libationAssets.filter((a) => {
|
||||
const p = d.value.parseAssetFilename(a.name)
|
||||
if (!p) return false
|
||||
if (os === 'windows' || os === 'mac' || os === 'linux') {
|
||||
if (p.platform !== os) return false
|
||||
}
|
||||
if (arch === 'x64' || arch === 'arm64') {
|
||||
if (p.arch !== arch) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
watch(previewValue, () => updateRecommendation())
|
||||
|
||||
onMounted(() => {
|
||||
d.value = getDetectOS()
|
||||
fetch('https://api.github.com/repos/rmcrackan/Libation/releases/latest')
|
||||
.then((res) => (res.ok ? res.json() : Promise.reject(new Error('Failed to load release'))))
|
||||
.then((release) => {
|
||||
cachedAssets.value = (release.assets || []).map((a) => ({
|
||||
name: a.name,
|
||||
url: a.browser_download_url,
|
||||
}))
|
||||
status.value = 'ready'
|
||||
updateRecommendation()
|
||||
})
|
||||
.catch((err) => {
|
||||
status.value = 'error'
|
||||
errorMessage.value = err.message || 'Could not load latest release.'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="download-widget-wrapper">
|
||||
<div v-if="status === 'loading'" class="download-widget recommended">
|
||||
Loading latest release…
|
||||
</div>
|
||||
<div v-else-if="status === 'error'" class="download-widget recommended">
|
||||
<p class="warn">{{ errorMessage }}</p>
|
||||
</div>
|
||||
<div v-else class="download-widget recommended">
|
||||
<div class="block-head">
|
||||
<svg class="icon-download" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
<span>{{ blockHead }}{{ rec?.windowsX64Choice ? ':' : rec?.recommended ? ':' : rec ? '.' : '' }}</span>
|
||||
<select v-model="previewValue" aria-label="Find recommended download for" class="preview-select">
|
||||
<option v-for="o in PREVIEW_OPTIONS" :key="o.value || 'current'" :value="o.value">
|
||||
{{ o.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Windows x64: Chardonnay + Classic -->
|
||||
<template v-if="rec?.windowsX64Choice">
|
||||
<ul class="widget-list">
|
||||
<li><strong>Chardonnay</strong> (modern UI): <a :href="rec.windowsX64Choice.chardonnay.url" download>{{ rec.windowsX64Choice.chardonnay.name }}</a></li>
|
||||
<li><strong>Classic</strong> (compact, screenreader-friendly): <a :href="rec.windowsX64Choice.classic.url" download>{{ rec.windowsX64Choice.classic.name }}</a></li>
|
||||
</ul>
|
||||
<p class="widget-link"><a :href="rec.classicVsChardonnayFaqUrl" target="_blank" rel="noopener">What's the difference between Classic and Chardonnay?</a></p>
|
||||
<p v-if="showReasonWarn" class="warn">{{ rec.reason }}</p>
|
||||
</template>
|
||||
|
||||
<!-- Single recommended -->
|
||||
<template v-else-if="rec?.recommended">
|
||||
<p class="widget-download"><a :href="rec.recommended.url" download>{{ rec.recommended.name }}</a></p>
|
||||
<p v-if="installGuideUrl" class="widget-link">
|
||||
<a :href="installGuideUrl" target="_blank" rel="noopener">{{ installGuideLabel }}</a>
|
||||
</p>
|
||||
<p v-if="showReasonWarn" class="warn">{{ rec.reason }}</p>
|
||||
<div v-if="rec.alternatives?.length" class="alternatives">
|
||||
{{ rec.alternatives.length === 1 ? 'Another option:' : 'Other options:' }}
|
||||
<ul>
|
||||
<li v-for="a in rec.alternatives" :key="a.name">
|
||||
<a :href="a.url" download>{{ a.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- No match -->
|
||||
<template v-else-if="rec">
|
||||
<p class="warn">{{ rec.reason || 'Could not recommend a download.' }}</p>
|
||||
<div class="alternatives">
|
||||
Available:
|
||||
<ul>
|
||||
<li v-for="a in showAssets" :key="a.name">
|
||||
<a :href="a.url" download>{{ a.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.download-widget-wrapper {
|
||||
margin: 1.5rem 0;
|
||||
max-width: 42rem;
|
||||
}
|
||||
.download-widget.recommended {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.block-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.block-head .icon-download {
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.preview-select {
|
||||
margin-left: auto;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.widget-list {
|
||||
margin: 0 0 0 1rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
.widget-download {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
.widget-link {
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
.warn {
|
||||
background: var(--vp-custom-block-warning-bg);
|
||||
color: var(--vp-custom-block-warning-text);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
.alternatives {
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.alternatives ul {
|
||||
margin: 0.25rem 0 0 1rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
19
.vitepress/theme/Layout.vue
Normal file
19
.vitepress/theme/Layout.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { ClientOnly } from 'vitepress/components'
|
||||
import DownloadSuggestionWidget from './DownloadSuggestionWidget.vue'
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { frontmatter } = useData()
|
||||
const { Layout: DefaultLayout } = DefaultTheme
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<template #home-hero-after>
|
||||
<ClientOnly>
|
||||
<DownloadSuggestionWidget v-if="frontmatter.layout === 'home'" />
|
||||
</ClientOnly>
|
||||
</template>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
196
.vitepress/theme/download-detection.js
Normal file
196
.vitepress/theme/download-detection.js
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Client-side OS and system detection for download recommendation.
|
||||
* Uses navigator.userAgent and (when available) navigator.userAgentData.
|
||||
* Call getDetectOS() in the browser only (e.g. in Vue onMounted).
|
||||
*/
|
||||
export function getDetectOS() {
|
||||
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''
|
||||
const uaData = typeof navigator !== 'undefined' && navigator.userAgentData
|
||||
|
||||
function getOS() {
|
||||
if (uaData?.platform) {
|
||||
const p = uaData.platform.toLowerCase()
|
||||
if (p === 'windows') return 'windows'
|
||||
if (p === 'macos') return 'mac'
|
||||
if (p === 'linux') return 'linux'
|
||||
if (p === 'android') return 'android'
|
||||
if (p === 'ios') return 'ios'
|
||||
}
|
||||
if (/\bWindows\b/i.test(ua)) return 'windows'
|
||||
if (/\bMac\b/i.test(ua) || /\bMacintosh\b/i.test(ua)) return 'mac'
|
||||
if (/\bLinux\b/i.test(ua) && !/\bAndroid\b/i.test(ua)) return 'linux'
|
||||
if (/\bAndroid\b/i.test(ua)) return 'android'
|
||||
if (/\b(iPhone|iPad|iPod)\b/i.test(ua)) return 'ios'
|
||||
return 'other'
|
||||
}
|
||||
|
||||
function getOSVersion() {
|
||||
const os = getOS()
|
||||
if (os === 'windows') {
|
||||
const ntMatch = ua.match(/Windows NT (\d+\.\d+)/)
|
||||
if (ntMatch) {
|
||||
const nt = ntMatch[1]
|
||||
if (nt === '10.0') return '10 or 11 (NT 10.0)'
|
||||
if (nt === '6.3') return '8.1'
|
||||
if (nt === '6.2') return '8'
|
||||
if (nt === '6.1') return '7'
|
||||
return `NT ${nt}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (os === 'mac') {
|
||||
const macMatch = ua.match(/Mac OS X (\d+[._]\d+(?:[._]\d+)?)/)
|
||||
return macMatch ? macMatch[1].replace(/_/g, '.') : null
|
||||
}
|
||||
if (os === 'linux') {
|
||||
const linuxMatch = ua.match(/Linux ([^\s;)]+)/)
|
||||
return linuxMatch ? linuxMatch[1] : null
|
||||
}
|
||||
if (os === 'android') {
|
||||
const m = ua.match(/Android (\d+(?:\.\d+)*)/)
|
||||
return m ? m[1] : null
|
||||
}
|
||||
if (os === 'ios') {
|
||||
const m = ua.match(/OS (\d+[._]\d+(?:[._]\d+)?)/)
|
||||
return m ? m[1].replace(/_/g, '.') : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getArchitecture() {
|
||||
if (uaData?.architecture) {
|
||||
const a = uaData.architecture.toLowerCase()
|
||||
if (a === 'x86' || a === 'amd64') return a === 'amd64' ? 'x64' : 'x86'
|
||||
if (a === 'arm' || a === 'arm64' || a === 'aarch64') return a === 'aarch64' ? 'arm64' : a
|
||||
return a
|
||||
}
|
||||
if (/\b(aarch64|arm64|ARM64)\b/i.test(ua)) return 'arm64'
|
||||
if (/\b(arm|aarch32)\b/i.test(ua) && !/arm64/i.test(ua)) return 'arm'
|
||||
if (/\b(Wow64|Win64|x64|x86_64|amd64)\b/i.test(ua)) return 'x64'
|
||||
if (/\b(WOW32|Win32|x86)\b/i.test(ua)) return 'x86'
|
||||
return 'other'
|
||||
}
|
||||
|
||||
function getLinuxFlavor() {
|
||||
if (getOS() !== 'linux') return null
|
||||
if (/\bUbuntu\b/i.test(ua)) return 'Ubuntu'
|
||||
if (/\bFedora\b/i.test(ua)) return 'Fedora'
|
||||
if (/\bDebian\b/i.test(ua)) return 'Debian'
|
||||
if (/\bLinux Mint\b/i.test(ua)) return 'Linux Mint'
|
||||
if (/\bOpenSUSE\b/i.test(ua)) return 'OpenSUSE'
|
||||
if (/\bArch\b/i.test(ua)) return 'Arch'
|
||||
if (/\bCentOS\b/i.test(ua)) return 'CentOS'
|
||||
if (/\bRed Hat\b/i.test(ua)) return 'Red Hat'
|
||||
if (/\bChrome OS\b/i.test(ua) || /\bCrOS\b/i.test(ua)) return 'Chrome OS'
|
||||
return null
|
||||
}
|
||||
|
||||
function parseAssetFilename(name) {
|
||||
if (!name || typeof name !== 'string') return null
|
||||
const lower = name.toLowerCase()
|
||||
let platform = null
|
||||
if (lower.includes('windows')) platform = 'windows'
|
||||
else if (lower.includes('macos') || (lower.includes('mac') && !lower.includes('macos'))) platform = 'mac'
|
||||
else if (lower.includes('linux')) platform = 'linux'
|
||||
if (!platform) return null
|
||||
let arch = null
|
||||
if (lower.includes('arm64') || lower.includes('aarch64')) arch = 'arm64'
|
||||
else if (lower.includes('amd64') || lower.includes('x64')) arch = 'x64'
|
||||
else if (lower.includes('x86') || lower.includes('i386')) arch = 'x86'
|
||||
if (!arch) return null
|
||||
let packageType = null
|
||||
if (lower.endsWith('.deb')) packageType = 'deb'
|
||||
else if (lower.endsWith('.rpm')) packageType = 'rpm'
|
||||
else if (lower.endsWith('.dmg')) packageType = 'dmg'
|
||||
else if (lower.endsWith('.zip') || lower.endsWith('.msi')) packageType = 'archive'
|
||||
return { platform, arch, packageType: packageType || 'unknown', name }
|
||||
}
|
||||
|
||||
function recommendDownload(assets, overrides) {
|
||||
const os = (overrides && overrides.os != null) ? overrides.os : getOS()
|
||||
const arch = (overrides && overrides.architecture != null) ? overrides.architecture : getArchitecture()
|
||||
const linuxFlavor = (overrides && overrides.linuxFlavor !== undefined) ? overrides.linuxFlavor : getLinuxFlavor()
|
||||
let reason = ''
|
||||
if (!Array.isArray(assets) || assets.length === 0) {
|
||||
return { recommended: null, alternatives: [], reason: reason || 'No assets provided.' }
|
||||
}
|
||||
const wantArch = (arch === 'x64' || arch === 'x86') ? 'x64' : (arch === 'arm64' || arch === 'arm') ? 'arm64' : null
|
||||
const wantOs = os === 'windows' ? 'windows' : os === 'mac' ? 'mac' : os === 'linux' ? 'linux' : null
|
||||
const preferDeb = linuxFlavor === 'Ubuntu' || linuxFlavor === 'Debian' || linuxFlavor === 'Linux Mint' || linuxFlavor === 'Chrome OS'
|
||||
const preferRpm = linuxFlavor === 'Fedora' || linuxFlavor === 'Red Hat' || linuxFlavor === 'CentOS' || linuxFlavor === 'OpenSUSE'
|
||||
const parsed = []
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
const a = assets[i]
|
||||
const name = a && (a.name != null ? a.name : a)
|
||||
const url = a && a.url
|
||||
const p = parseAssetFilename(name)
|
||||
if (!p) continue
|
||||
parsed.push({ name, url, parsed: p })
|
||||
}
|
||||
if (parsed.length === 0) {
|
||||
return { recommended: null, alternatives: [], reason: reason || 'No recognizable asset filenames.' }
|
||||
}
|
||||
function score(entry) {
|
||||
const p = entry.parsed
|
||||
const nameLower = (entry.name || '').toLowerCase()
|
||||
let s = 0
|
||||
if (wantOs && p.platform === wantOs) s += 100
|
||||
else if (wantOs) s -= 50
|
||||
if (wantArch && p.arch === wantArch) s += 50
|
||||
else if (wantArch) s -= 30
|
||||
if (wantOs === 'linux' && p.packageType === 'deb' && preferDeb) s += 20
|
||||
if (wantOs === 'linux' && p.packageType === 'rpm' && preferRpm) s += 20
|
||||
if (wantOs === 'linux' && !preferDeb && !preferRpm && p.packageType === 'deb') s += 5
|
||||
if (nameLower.includes('chardonnay')) s += 10
|
||||
if (nameLower.includes('classic')) s -= 5
|
||||
return s
|
||||
}
|
||||
parsed.sort((a, b) => score(b) - score(a))
|
||||
const best = parsed[0]
|
||||
const bestScore = score(best)
|
||||
const recommended = bestScore >= 50 ? { name: best.name, url: best.url } : null
|
||||
let altCandidates = parsed.slice(1, 4)
|
||||
if (wantOs) altCandidates = altCandidates.filter(e => e.parsed.platform === wantOs)
|
||||
if (wantArch) altCandidates = altCandidates.filter(e => e.parsed.arch === wantArch)
|
||||
const alternatives = altCandidates.map(e => ({ name: e.name, url: e.url }))
|
||||
const classicVsChardonnayFaqUrl = 'https://getlibation.com/docs/frequently-asked-questions#what-s-the-difference-between-classic-and-chardonnay'
|
||||
let windowsX64Choice = null
|
||||
if (wantOs === 'windows' && wantArch === 'x64') {
|
||||
let winChardonnay = null
|
||||
let winClassic = null
|
||||
for (let j = 0; j < parsed.length; j++) {
|
||||
const e = parsed[j]
|
||||
if (e.parsed.platform !== 'windows' || e.parsed.arch !== 'x64') continue
|
||||
const n = (e.name || '').toLowerCase()
|
||||
if (n.includes('chardonnay')) winChardonnay = { name: e.name, url: e.url }
|
||||
if (n.includes('classic')) winClassic = { name: e.name, url: e.url }
|
||||
}
|
||||
if (winChardonnay && winClassic) windowsX64Choice = { chardonnay: winChardonnay, classic: winClassic }
|
||||
}
|
||||
if (!recommended && wantOs) {
|
||||
reason = 'No asset matched your system (OS: ' + os + ', arch: ' + arch + '). Try choosing a download manually.'
|
||||
} else if (recommended && wantOs && !wantArch) {
|
||||
reason = 'Matched your OS; architecture could not be detected. If this download is wrong, pick another (e.g. arm64 vs x64).'
|
||||
}
|
||||
return {
|
||||
recommended,
|
||||
alternatives,
|
||||
reason: reason || (recommended ? 'Recommended for your system.' : ''),
|
||||
windowsX64Choice,
|
||||
classicVsChardonnayFaqUrl: windowsX64Choice ? classicVsChardonnayFaqUrl : null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
os: getOS(),
|
||||
osVersion: getOSVersion(),
|
||||
architecture: getArchitecture(),
|
||||
linuxFlavor: getLinuxFlavor(),
|
||||
getOS,
|
||||
getOSVersion,
|
||||
getArchitecture,
|
||||
getLinuxFlavor,
|
||||
parseAssetFilename,
|
||||
recommendDownload,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import Layout from './Layout.vue'
|
||||
import './custom.css'
|
||||
|
||||
export default DefaultTheme
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
**Libation** is a free, open-source application for downloading and managing your Audible audiobooks. It decrypts your library, removes DRM, and lets you own your audiobooks forever.
|
||||
|
||||
> **[Which version should I download?](https://getlibation.com)** — get a recommended download for your system on our site.
|
||||
|
||||
## Features
|
||||
|
||||
- **Unlock Your Library**: Download and remove DRM from your audiobooks.
|
||||
|
||||
Reference in New Issue
Block a user