[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Update website

Kasper (Mar 11, 2026, 10:41 AM +0100) 4cfd364e 582e9053

+364 -134
+364 -134
website/src/routes/Download.svelte
··· 10 10 arch: string 11 11 ending: string 12 12 } 13 - const macOsArm: Version = { 14 - os: 'macOS', 15 - arch: 'arm64', 16 - ending: '-mac-arm64.dmg', 17 - } 18 - const macOsX64: Version = { 19 - os: 'macOS', 20 - arch: 'x64', 21 - ending: '-mac-x64.dmg', 22 - } 23 - const windowsX64: Version = { 24 - os: 'Windows', 25 - arch: 'x64', 26 - ending: '-win-x64.exe', 27 - } 28 - const windowsArm: Version = { 29 - os: 'Windows', 30 - arch: 'arm64', 31 - ending: '-win-arm64.exe', 32 - } 33 - const linuxDeb: Version = { 34 - os: 'Linux .deb', 35 - arch: 'x64', 36 - ending: '-linux-amd64.deb', 37 - } 38 - const linuxlinuxRpm: Version = { 39 - os: 'Linux .rpm', 40 - arch: 'x64', 41 - ending: 'linux-x86_64.rpm', 42 - } 43 - const versionList: Version[] = [ 44 - macOsArm, 45 - macOsX64, 46 - windowsX64, 47 - windowsArm, 48 - linuxDeb, 49 - linuxlinuxRpm, 13 + const macos_arm: Version = { os: 'macOS', arch: 'arm64', ending: '-mac-arm64.dmg' } 14 + const macos_x64: Version = { os: 'macOS', arch: 'x64', ending: '-mac-x64.dmg' } 15 + const windows_x64: Version = { os: 'Windows', arch: 'x64', ending: '-win-x64.exe' } 16 + const windows_arm: Version = { os: 'Windows', arch: 'arm64', ending: '-win-arm64.exe' } 17 + const linux_deb: Version = { os: 'Linux', arch: '.deb', ending: '-linux-amd64.deb' } 18 + const linux_rpm: Version = { os: 'Linux', arch: '.rpm', ending: 'linux-x86_64.rpm' } 19 + const android_obtainium: Version = { os: 'Obtainium', arch: '', ending: '' } 20 + const android_arm64: Version = { os: 'Android', arch: 'arm64', ending: '-android-arm64.apk' } 21 + const android_x86_64: Version = { os: 'Android', arch: 'x86_64', ending: '-android-x86_64.apk' } 22 + 23 + const desktop_versions: Version[] = [ 24 + macos_arm, 25 + macos_x64, 26 + windows_x64, 27 + windows_arm, 28 + linux_deb, 29 + linux_rpm, 50 30 ] 51 - let suggestedVersion = windowsX64 31 + const android_versions: Version[] = [android_obtainium, android_arm64, android_x86_64] 32 + 33 + const obtainium_url = 34 + 'https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22space.kasper.ferrum%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fprobablykasper%2Fferrum%22%2C%22author%22%3A%22probablykasper%22%2C%22name%22%3A%22Ferrum%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22overrideSource%22%3A%22GitHub%22%7D' 35 + 36 + let suggested_desktop = windows_x64 37 + 52 38 onMount(() => { 53 39 const browser = Bowser.getParser(window.navigator.userAgent) 54 - const osName = browser.getOSName() 55 - if (osName === 'macOS' || osName === 'iOS') { 56 - suggestedVersion = macOsArm 57 - } else if (osName === 'Windows') { 58 - suggestedVersion = windowsX64 59 - } else if (osName === 'Linux' || osName === 'Chrome OS') { 60 - suggestedVersion = linuxDeb 40 + const os_name = browser.getOSName() 41 + if (os_name === 'macOS' || os_name === 'iOS') { 42 + suggested_desktop = macos_arm 43 + } else if (os_name === 'Windows') { 44 + suggested_desktop = windows_x64 45 + } else if (os_name === 'Linux' || os_name === 'Chrome OS') { 46 + suggested_desktop = linux_deb 61 47 } 62 48 }) 63 49 64 - let downloadError = '' 50 + let download_error = '' 65 51 let loading = false 66 52 type LatestRelease = RestEndpointMethodTypes['repos']['getLatestRelease']['response']['data'] 67 53 68 - function getAsset(assets: LatestRelease['assets'], version: Version) { 54 + function get_asset(assets: LatestRelease['assets'], version: Version) { 69 55 for (const asset of assets) { 70 - if (asset.name.toLowerCase().endsWith(version.ending)) { 71 - return asset 72 - } 56 + if (asset.name.toLowerCase().endsWith(version.ending)) return asset 73 57 } 74 58 return null 75 59 } 76 - async function innerDownload(version: Version) { 77 - downloadError = '' 60 + 61 + async function inner_download(version: Version) { 62 + download_error = '' 63 + if (version.os === 'Obtainium') { 64 + window.open(obtainium_url, '_blank', 'noopener,noreferrer') 65 + return 66 + } 78 67 try { 79 68 const response = await fetch( 80 - `https://api.github.com/repos/probablykasper/ferrum/releases/latest`, 69 + 'https://api.github.com/repos/probablykasper/ferrum/releases/latest', 81 70 ) 82 - console.log(response) 83 - 84 71 const json: LatestRelease = await response.json() 85 - console.log(json) 86 - 87 72 if (response.status !== 200) { 88 - downloadError = 'Could not download' 73 + download_error = 'Could not download' 89 74 console.error('Error response from GitHub', json) 90 75 return 91 76 } 92 - 93 - const asset = getAsset(json.assets, version) 77 + const asset = get_asset(json.assets, version) 94 78 if (asset === null) { 95 - downloadError = 'No file found' 79 + download_error = 'No file found' 96 80 console.error('No file found') 97 81 return 98 82 } 99 - console.log(asset) 100 83 location.href = asset.browser_download_url 101 84 } catch (error) { 102 - downloadError = 'Network error' 85 + download_error = 'Network error' 103 86 console.error(error) 104 87 } 105 88 } 89 + 106 90 async function download(version: Version) { 107 91 loading = true 108 - await innerDownload(version) 92 + await inner_download(version) 109 93 loading = false 110 94 } 111 95 </script> 112 96 113 - <div class="mb-2 h-6"> 114 - {#if downloadError} 115 - <div 116 - class="error block text-center text-sm font-semibold text-red-500" 117 - transition:fly={{ y: 3, duration: 200 }} 97 + <!-- Hidden SVG sprite sheet for all logos --> 98 + <svg style="display:none" aria-hidden="true"> 99 + <!-- Tabler filled Apple --> 100 + <symbol id="icon-apple" viewBox="0 0 24 24" fill="currentColor"> 101 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 102 + <path 103 + d="M15.079 5.999l.239 .012c1.43 .097 3.434 1.013 4.508 2.586a1 1 0 0 1 -.344 1.44c-.05 .028 -.372 .158 -.497 .217a4.15 4.15 0 0 0 -.722 .431c-.614 .461 -.948 1.009 -.942 1.694c.01 .885 .339 1.454 .907 1.846c.208 .143 .436 .253 .666 .33c.126 .043 .426 .116 .444 .122a1 1 0 0 1 .662 .942c0 2.621 -3.04 6.381 -5.286 6.381c-.79 0 -1.272 -.091 -1.983 -.315l-.098 -.031c-.463 -.146 -.702 -.192 -1.133 -.192c-.52 0 -.863 .06 -1.518 .237l-.197 .053c-.575 .153 -.964 .226 -1.5 .248c-2.749 0 -5.285 -5.093 -5.285 -9.072c0 -3.87 1.786 -6.92 5.286 -6.92c.297 0 .598 .045 .909 .128c.403 .107 .774 .26 1.296 .508c.787 .374 .948 .44 1.009 .44h.016c.03 -.003 .128 -.047 1.056 -.457c1.061 -.467 1.864 -.685 2.746 -.616l-.24 -.012z" 104 + /> 105 + <path d="M14 1a1 1 0 0 1 1 1a3 3 0 0 1 -3 3a1 1 0 0 1 -1 -1a3 3 0 0 1 3 -3z" /> 106 + </symbol> 107 + <!-- Tabler filled Windows --> 108 + <symbol id="icon-windows" viewBox="0 0 24 24" fill="currentColor"> 109 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 110 + <path 111 + d="M21 13v5c0 1.57 -1.248 2.832 -2.715 2.923l-.113 .003l-.042 .018a1 1 0 0 1 -.336 .056l-.118 -.008l-4.676 -.585v-7.407zm-10 0v7.157l-5.3 -.662c-1.514 -.151 -2.7 -1.383 -2.7 -2.895v-3.6zm0 -9.158v7.158h-8v-3.6c0 -1.454 1.096 -2.648 2.505 -2.87zm10 2.058v5.1h-8v-7.409l4.717 -.589c1.759 -.145 3.283 1.189 3.283 2.898" 112 + /> 113 + </symbol> 114 + <!-- Custom Tux --> 115 + <symbol id="icon-linux" viewBox="3 2 18 20" fill="currentColor"> 116 + <path 117 + d="M6.09159 20.5316C7.0409 20.645 8.10753 21.2599 9.00016 21.3685C9.89759 21.4818 10.1753 20.7574 10.1753 20.7574C10.1753 20.7574 11.1852 20.5316 12.2469 20.5056C13.3096 20.4758 14.3156 20.7266 14.3156 20.7266C14.3156 20.7266 14.5107 21.1734 14.8749 21.3685C15.2391 21.5674 16.0231 21.5943 16.5256 21.0648C17.0291 20.5316 18.3724 19.8599 19.1267 19.44C19.8858 19.0191 19.7465 18.3773 19.2699 18.1822C18.7934 17.9872 18.4032 17.6797 18.433 17.0897C18.459 16.5046 18.0122 16.1145 18.0122 16.1145C18.0122 16.1145 18.4032 14.8269 18.0391 13.7603C17.6749 12.6986 16.4738 10.9911 15.5504 9.70743C14.6271 8.41988 15.4111 6.93343 14.5704 5.03377C13.7296 3.13126 11.5494 3.24371 10.3743 4.05463C9.19913 4.8656 9.55942 6.87668 9.6161 7.83085C9.67279 8.78017 9.64204 9.45948 9.53347 9.7036C9.4249 9.95148 8.66679 10.8518 8.1633 11.6061C7.66079 12.3642 7.29656 13.9295 6.92856 14.5752C6.56822 15.2171 6.81999 15.8022 6.81999 15.8022C6.81999 15.8022 6.56827 15.8887 6.36936 16.3096C6.17427 16.7256 5.78416 16.9245 5.08176 17.0601C4.38416 17.2032 4.38416 17.6529 4.55233 18.1564C4.72142 18.6589 4.55233 18.9404 4.35725 19.5823C4.16222 20.2241 5.13845 20.4191 6.09159 20.5316ZM15.9597 17.6038C16.4584 17.8219 17.1752 17.5182 17.3933 17.3001C17.6105 17.083 17.7642 16.7601 17.7642 16.7601C17.7642 16.7601 17.9823 16.8687 17.9602 17.2136C17.9371 17.5634 18.1101 18.062 18.4368 18.235C18.7635 18.407 19.2622 18.6472 19.0037 18.8874C18.7404 19.1276 17.2838 19.7138 16.8485 20.1711C16.4171 20.6256 15.8502 20.9975 15.5052 20.8879C15.1564 20.7794 14.8518 20.3028 15.0018 19.6052C15.1564 18.9105 15.2871 18.1485 15.265 17.7132C15.242 17.278 15.1564 16.6919 15.265 16.6054C15.3736 16.5198 15.5466 16.5611 15.5466 16.5611C15.5466 16.5611 15.46 17.3867 15.9597 17.6038ZM12.7235 5.84474C13.2039 5.84474 13.5912 6.32131 13.5912 6.90743C13.5912 7.32348 13.3961 7.68383 13.1107 7.8568C13.0387 7.82703 12.9637 7.79337 12.8811 7.75874C13.054 7.67325 13.1741 7.45514 13.1741 7.20337C13.1741 6.87383 12.9714 6.60286 12.7167 6.60286C12.4688 6.60286 12.2622 6.87377 12.2622 7.20337C12.2622 7.32354 12.292 7.4436 12.341 7.53777C12.1911 7.4772 12.0556 7.42537 11.9471 7.38405C11.8904 7.24085 11.8567 7.07943 11.8567 6.90748C11.8568 6.32137 12.243 5.84474 12.7235 5.84474ZM11.5339 7.67617C11.7703 7.71748 12.4198 7.99903 12.6601 8.08548C12.9003 8.16811 13.1664 8.32183 13.1405 8.4756C13.1107 8.63411 12.9868 8.63411 12.6601 8.83303C12.3372 9.02811 11.632 9.46337 11.4062 9.49314C11.1813 9.52291 11.0535 9.39605 10.8132 9.24137C10.573 9.08377 10.1224 8.71577 10.2358 8.52074C10.2358 8.52074 10.5884 8.25074 10.7422 8.1124C10.8959 7.9692 11.2937 7.63194 11.5339 7.67617ZM10.4981 6.01388C10.8767 6.01388 11.1851 6.46451 11.1851 7.01988C11.1851 7.1208 11.1736 7.21491 11.1554 7.30908C11.0612 7.33886 10.9671 7.38788 10.8767 7.46668C10.8325 7.50417 10.7911 7.53777 10.7537 7.57526C10.8132 7.46286 10.8363 7.30143 10.8094 7.13228C10.7575 6.83251 10.5548 6.61057 10.3559 6.64034C10.156 6.674 10.0368 6.9488 10.0849 7.25246C10.1378 7.55994 10.3366 7.78183 10.5394 7.74823C10.5509 7.7444 10.5615 7.74057 10.573 7.73668C10.476 7.83085 10.3856 7.91348 10.2915 7.98074C10.0176 7.85291 9.8149 7.47051 9.8149 7.01988C9.81496 6.46068 10.1186 6.01388 10.4981 6.01388ZM8.38902 13.2838C8.77913 12.6689 9.03084 11.3246 9.42096 10.8778C9.81496 10.4319 10.1186 9.48165 9.98021 9.06177C9.98021 9.06177 10.821 10.0678 11.4062 9.90251C11.9923 9.73337 13.3096 8.75428 13.5047 8.92245C13.6998 9.09154 15.3774 12.7813 15.5465 13.9564C15.7156 15.1306 15.4341 16.0281 15.4341 16.0281C15.4341 16.0281 14.7923 15.8589 14.7096 16.249C14.627 16.643 14.627 18.0699 14.627 18.0699C14.627 18.0699 13.7593 19.271 12.416 19.4698C11.0727 19.6649 10.4 19.5227 10.4 19.5227L9.64576 18.6588C9.64576 18.6588 10.2319 18.5724 10.1492 17.9834C10.0666 17.3982 8.35816 16.5872 8.05067 15.8589C7.7433 15.1305 7.9941 13.8997 8.38902 13.2838ZM5.0673 17.6903C5.13456 17.402 6.00507 17.4021 6.33947 17.1993C6.67387 16.9966 6.74107 16.4143 7.01113 16.2606C7.27724 16.103 7.76924 16.6622 7.97199 16.9773C8.1709 17.2848 8.9329 18.6291 9.24422 18.9634C9.55936 19.3007 9.84862 19.7475 9.75827 20.1491C9.67279 20.5507 9.19907 20.8438 9.19907 20.8438C8.77536 20.9745 7.59347 20.4643 7.05639 20.2394C6.5193 20.0136 5.1529 19.9463 4.97707 19.7475C4.79645 19.5447 5.06353 19.0979 5.13462 18.6742C5.19799 18.2456 4.99913 17.9795 5.0673 17.6903Z" 118 + /> 119 + </symbol> 120 + <!-- Tabler outline desktop --> 121 + <symbol 122 + id="icon-desktop" 123 + viewBox="0 0 24 24" 124 + fill="none" 125 + stroke="currentColor" 126 + stroke-width="2" 127 + stroke-linecap="round" 128 + stroke-linejoin="round" 129 + > 130 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 131 + <path d="M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10" /> 132 + <path d="M7 20h10" /><path d="M9 16v4" /><path d="M15 16v4" /> 133 + </symbol> 134 + <!-- Tabler outline Android --> 135 + <symbol 136 + id="icon-android" 137 + viewBox="0 0 24 24" 138 + fill="none" 139 + stroke="currentColor" 140 + stroke-width="2" 141 + stroke-linecap="round" 142 + stroke-linejoin="round" 143 + > 144 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 145 + <path d="M4 10l0 6" /><path d="M20 10l0 6" /> 146 + <path d="M7 9h10v8a1 1 0 0 1 -1 1h-8a1 1 0 0 1 -1 -1v-8a5 5 0 0 1 10 0" /> 147 + <path d="M8 3l1 2" /><path d="M16 3l-1 2" /> 148 + <path d="M9 18l0 3" /><path d="M15 18l0 3" /> 149 + </symbol> 150 + <!-- Obtainium real logo --> 151 + <symbol id="icon-obtainium" viewBox="30.204 75 512.59 471.09"> 152 + <path 153 + fill="currentColor" 154 + d="M380.1 542.6c-6.5-3.4-44.3-24.4-221.4-122.7-49.3-27.4-96.8-53.7-105.5-58.5-19.8-10.9-22.8-13.6-22.8-20.1 0-3.4 7.9-25.4 26.4-73.5 14.5-37.7 27.4-70.8 28.5-73.5 2.7-6.1 7-11.3 11.2-13.5 3.9-2 102.4-38 107.5-39.2 5.4-1.3 10.1 1.2 12.9 7.1 1.3 2.8 6.8 17.2 12.2 32.1s13.4 36.8 17.9 48.8c10.1 27.5 10.5 31.4 3.5 35.5-1.9 1.1-14.1 5.9-27.1 10.6s-25.4 9.6-27.6 10.9c-5 2.9-5.7 7.4-1.8 11 1.6 1.4 26.6 15.7 55.6 31.7 81.2 44.8 76.7 42.7 82.1 36.9 2.7-2.9 51.1-126.2 53.9-137.2 1.2-4.7 1.1-5.3-1.5-7.4-3.5-2.8-3-2.9-34.8 8.3-31.6 11.2-34.6 11.4-38.8 3.1-2.2-4.4-36.2-97.8-38.3-105.3-1.4-4.9.3-9.5 4.4-12.2 1.7-1.1 26.5-10.6 55-21.1 42.4-15.6 53.1-19.1 58.7-19.2 7.1-.1 8 .4 138.4 72.5 9 5 13.9 10.3 13.9 15.1 0 1.9-32.3 87.7-71.8 190.5-55.7 145.1-72.5 187.6-75 189.8-4.3 3.9-7.7 3.7-15.7-.5z" 155 + /> 156 + </symbol> 157 + <!-- Tabler outline package --> 158 + <symbol 159 + id="icon-apk" 160 + viewBox="0 0 24 24" 161 + fill="none" 162 + stroke="currentColor" 163 + stroke-width="2" 164 + stroke-linecap="round" 165 + stroke-linejoin="round" 166 + > 167 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 168 + <path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" /> 169 + <path d="M12 12l8 -4.5" /><path d="M12 12l0 9" /><path d="M12 12l-8 -4.5" /> 170 + <path d="M16 5.25l-8 4.5" /> 171 + </symbol> 172 + </svg> 173 + 174 + <div class="flex w-full flex-col items-center gap-3"> 175 + {#if download_error} 176 + <p 177 + class="error-glow text-sm font-semibold text-red-400" 178 + transition:fly={{ y: -4, duration: 200 }} 118 179 > 119 - {downloadError} 120 - </div> 180 + {download_error} 181 + </p> 121 182 {/if} 122 - </div> 123 183 124 - <ButtonPopup let:toggle let:isOpen let:close> 125 184 <div 126 - class="relative mx-auto flex h-9 items-center border border-white/10 bg-white/5 text-base text-[15px] font-medium transition-all duration-200 ease-in-out hover:border-white/20" 127 - class:rounded-2xl={!isOpen} 128 - class:rounded-lg={isOpen} 129 - class:pointer-events-none={loading} 130 - class:opacity-75={loading} 185 + class="flex w-full max-w-xl items-stretch rounded-2xl border border-white/10 bg-white/[0.04]" 131 186 > 132 - <button 133 - class="group relative flex h-full cursor-pointer items-center pr-4 pl-5 text-white/70 outline-none transition-all duration-200 hover:text-white/100" 134 - on:click={() => download(suggestedVersion)} 135 - class:opacity-50={loading} 136 - > 187 + <!-- ── Desktop ── --> 188 + <div class="relative z-10 flex flex-1 flex-col gap-2.5 p-5"> 137 189 <div 138 - class="ease-out-cubic opacity-0 transition-all duration-700 group-hover:opacity-40 group-focus:opacity-40" 190 + class="flex items-center gap-1.5 text-[10px] font-semibold tracking-widest text-white/35 uppercase" 139 191 > 192 + <svg 193 + width="15" 194 + height="15" 195 + fill="none" 196 + stroke="currentColor" 197 + stroke-width="2" 198 + stroke-linecap="round" 199 + stroke-linejoin="round" 200 + aria-hidden="true"><use href="#icon-desktop" /></svg 201 + > 202 + Desktop 203 + </div> 204 + <p class="-mt-1 text-xs leading-snug text-white/0 select-none" aria-hidden="true">.</p> 205 + 206 + <ButtonPopup let:toggle let:isOpen let:close> 140 207 <div 141 - class="gradient gradient-3 ease-out-cubic absolute inset-0 -z-10 scale-80 transition-all duration-700 group-hover:scale-100 group-hover:blur-md group-focus:scale-100 group-focus:blur-md" 142 - ></div> 143 - </div> 144 - Download for {suggestedVersion.os} 145 - {suggestedVersion.arch} 146 - </button> 147 - <div class="h-5 border-l border-white/30"></div> 148 - <button 149 - class="group relative h-full cursor-pointer pr-5 pl-4 text-white/70 outline-none transition-all duration-200 hover:text-white/100" 150 - on:click={toggle} 151 - aria-label="Download for other platforms" 152 - > 208 + class="relative flex h-[38px] items-center border bg-white/[0.06] text-sm font-medium transition-[border-color,border-radius] duration-200 {isOpen 209 + ? 'rounded-t-[10px] border-white/20' 210 + : 'rounded-xl border-white/[0.12]'} {loading ? 'pointer-events-none opacity-60' : ''}" 211 + > 212 + <button 213 + type="button" 214 + class="flex h-full cursor-pointer items-center gap-1.5 pr-3.5 pl-3 whitespace-nowrap text-white/70 transition-colors duration-150 outline-none hover:text-white" 215 + on:click={() => download(suggested_desktop)} 216 + > 217 + <svg 218 + width="15" 219 + height="15" 220 + fill="currentColor" 221 + class="shrink-0 opacity-80" 222 + aria-hidden="true" 223 + > 224 + <use 225 + href={suggested_desktop.os === 'macOS' 226 + ? '#icon-apple' 227 + : suggested_desktop.os === 'Windows' 228 + ? '#icon-windows' 229 + : '#icon-linux'} 230 + /> 231 + </svg> 232 + Download for {suggested_desktop.os} 233 + <span class="text-xs text-white/40">{suggested_desktop.arch}</span> 234 + </button> 235 + <div class="h-5 w-px shrink-0 bg-white/15"></div> 236 + <button 237 + type="button" 238 + class="flex h-full cursor-pointer items-center justify-center px-3 text-white/50 transition-colors duration-150 outline-none hover:text-white/90" 239 + on:click={toggle} 240 + aria-label="Other desktop platforms" 241 + > 242 + <svg 243 + fill="currentColor" 244 + viewBox="0 0 24 24" 245 + width="11" 246 + height="11" 247 + class="chevron" 248 + class:rotated={isOpen} 249 + aria-hidden="true" 250 + > 251 + <path d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z" /> 252 + </svg> 253 + </button> 254 + </div> 255 + 256 + <div 257 + slot="popup" 258 + class="dropdown absolute top-full right-0 left-0 overflow-hidden rounded-b-[10px] border border-t-0 border-white/20 bg-white/5 backdrop-blur-xl" 259 + style="z-index: 9999;" 260 + transition:scale={{ start: 0.95, opacity: 0, duration: 160 }} 261 + > 262 + {#each desktop_versions as version} 263 + <button 264 + type="button" 265 + class="dropdown-item flex h-9 w-full cursor-pointer items-center gap-2 bg-transparent px-3.5 text-left text-[13.5px] text-white/80 transition-colors duration-100 outline-none hover:bg-white/[0.06] hover:text-white" 266 + on:click={() => { 267 + close() 268 + download(version) 269 + }} 270 + > 271 + <svg 272 + width="15" 273 + height="15" 274 + fill="currentColor" 275 + class="shrink-0 opacity-70" 276 + aria-hidden="true" 277 + > 278 + <use 279 + href={version.os === 'macOS' 280 + ? '#icon-apple' 281 + : version.os === 'Windows' 282 + ? '#icon-windows' 283 + : '#icon-linux'} 284 + /> 285 + </svg> 286 + <span class="flex-1">{version.os}</span> 287 + <span class="text-xs text-white/35">{version.arch}</span> 288 + </button> 289 + {/each} 290 + </div> 291 + </ButtonPopup> 292 + </div> 293 + 294 + <!-- vertical divider --> 295 + <div class="my-3.5 w-px shrink-0 bg-white/[0.08]"></div> 296 + 297 + <!-- ── Android ── --> 298 + <div class="relative z-[5] flex flex-1 flex-col gap-2.5 p-5"> 153 299 <div 154 - class="ease-out-cubic opacity-0 transition-all duration-700 group-hover:opacity-40 group-focus:opacity-40" 300 + class="flex items-center gap-1.5 text-[10px] font-semibold tracking-widest text-white/35 uppercase" 155 301 > 302 + <svg width="15" height="15" fill="currentColor" aria-hidden="true" 303 + ><use href="#icon-android" /></svg 304 + > 305 + Android 306 + </div> 307 + <p class="-mt-1 text-xs leading-snug text-white/30">Currently library browser only</p> 308 + 309 + <ButtonPopup let:toggle let:isOpen let:close> 156 310 <div 157 - class="gradient gradient-3 ease-out-cubic absolute inset-0 -z-10 scale-80 transition-all duration-700 group-hover:scale-100 group-hover:blur-md group-focus:scale-100 group-focus:blur-md" 158 - ></div> 159 - </div> 160 - <svg 161 - fill="currentColor" 162 - xmlns="http://www.w3.org/2000/svg" 163 - width="12" 164 - height="12" 165 - viewBox="0 0 24 24" 166 - ><path d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z" /></svg 167 - > 168 - </button> 169 - </div> 170 - <div 171 - slot="popup" 172 - class="mt-0.5 w-full divide-y divide-white/10 rounded-lg border border-white/20 bg-white/5 text-[15px] text-white backdrop-blur-md will-change-contents" 173 - transition:scale={{ start: 0.9, opacity: 0, duration: 200 }} 174 - > 175 - {#each versionList as version} 176 - <button 177 - class="focus:bg-opacity-5 h-9 w-full cursor-pointer bg-white/0 px-5 text-left outline-none hover:bg-white/5" 178 - on:click={() => { 179 - close() 180 - download(version) 181 - }} 182 - > 183 - {version.os} <span class="opacity-70">{version.arch}</span> 184 - </button> 185 - {/each} 311 + class="relative flex h-[38px] items-center border bg-white/[0.06] text-sm font-medium transition-[border-color,border-radius] duration-200 {isOpen 312 + ? 'rounded-t-[10px] border-white/20' 313 + : 'rounded-xl border-white/[0.12]'} {loading ? 'pointer-events-none opacity-60' : ''}" 314 + > 315 + <a 316 + href={obtainium_url} 317 + target="_blank" 318 + rel="noopener noreferrer" 319 + class="flex h-full cursor-pointer items-center gap-1.5 pr-3.5 pl-3 whitespace-nowrap text-white/70 transition-colors duration-150 outline-none hover:text-white" 320 + > 321 + <svg 322 + width="14" 323 + height="14" 324 + fill="none" 325 + stroke="currentColor" 326 + stroke-width="2" 327 + stroke-linecap="round" 328 + stroke-linejoin="round" 329 + class="shrink-0 opacity-75" 330 + aria-hidden="true" 331 + > 332 + <use href="#icon-obtainium" /> 333 + </svg> 334 + Get it on Obtainium 335 + </a> 336 + <div class="h-5 w-px shrink-0 bg-white/15"></div> 337 + <button 338 + type="button" 339 + class="flex h-full cursor-pointer items-center justify-center px-3 text-white/50 transition-colors duration-150 outline-none hover:text-white/90" 340 + on:click={toggle} 341 + aria-label="Other Android options" 342 + > 343 + <svg 344 + fill="currentColor" 345 + viewBox="0 0 24 24" 346 + width="11" 347 + height="11" 348 + class="chevron" 349 + class:rotated={isOpen} 350 + aria-hidden="true" 351 + > 352 + <path d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z" /> 353 + </svg> 354 + </button> 355 + </div> 356 + 357 + <div 358 + slot="popup" 359 + class="dropdown absolute top-full right-0 left-0 overflow-hidden rounded-b-[10px] border border-t-0 border-white/20 bg-white/5 backdrop-blur-xl" 360 + style="z-index: 9999;" 361 + transition:scale={{ start: 0.95, opacity: 0, duration: 160 }} 362 + > 363 + {#each android_versions as version} 364 + {#if version.os === 'Obtainium'} 365 + <a 366 + href={obtainium_url} 367 + target="_blank" 368 + rel="noopener noreferrer" 369 + class="dropdown-item flex h-9 w-full cursor-pointer items-center gap-2 bg-transparent px-3.5 text-left text-[13.5px] text-white/80 no-underline transition-colors duration-100 outline-none hover:bg-white/[0.06] hover:text-white" 370 + on:click={close} 371 + > 372 + <svg 373 + width="15" 374 + height="15" 375 + fill="currentColor" 376 + class="shrink-0 opacity-70" 377 + aria-hidden="true" 378 + > 379 + <use href="#icon-obtainium" /> 380 + </svg> 381 + <span class="flex-1">Obtainium</span> 382 + <span class="text-xs text-white/35">recommended</span> 383 + </a> 384 + {:else} 385 + <button 386 + type="button" 387 + class="dropdown-item flex h-9 w-full cursor-pointer items-center gap-2 bg-transparent px-3.5 text-left text-[13.5px] text-white/80 transition-colors duration-100 outline-none hover:bg-white/[0.06] hover:text-white" 388 + on:click={() => { 389 + close() 390 + download(version) 391 + }} 392 + > 393 + <svg 394 + width="15" 395 + height="15" 396 + fill="currentColor" 397 + class="shrink-0 opacity-70" 398 + aria-hidden="true" 399 + > 400 + <use href="#icon-apk" /> 401 + </svg> 402 + <span class="flex-1">APK</span> 403 + <span class="text-xs text-white/35">{version.arch}</span> 404 + </button> 405 + {/if} 406 + {/each} 407 + </div> 408 + </ButtonPopup> 409 + </div> 186 410 </div> 187 - </ButtonPopup> 411 + </div> 188 412 189 - <style lang="sass"> 190 - .gradient 191 - will-change: contents 192 - .gradient-3 193 - background: linear-gradient(130deg,#3EFF8A 10%,#03B5FF 90%) 194 - .error 195 - text-shadow: 0px 0px 7px hsl(0deg 100% 50% / 60%) 413 + <style> 414 + .error-glow { 415 + text-shadow: 0 0 8px hsl(0 100% 60% / 50%); 416 + } 417 + .chevron { 418 + transition: transform 0.2s ease; 419 + } 420 + .chevron.rotated { 421 + transform: rotate(180deg); 422 + } 423 + .dropdown-item + .dropdown-item { 424 + border-top: 1px solid rgba(255, 255, 255, 0.05); 425 + } 196 426 </style>