mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-20 23:24:52 +02:00
2143de3aa7
Strip doc comments, section dividers, HTML comments, and Flutter template boilerplate that add no informational value. No logic or behavior changes.
723 lines
45 KiB
HTML
723 lines
45 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Downloads - SpotiFLAC Mobile</title>
|
|
<meta name="description" content="Download the latest version of SpotiFLAC Mobile. Changelog and release history included.">
|
|
<meta name="theme-color" content="#0a0a0a">
|
|
<link rel="icon" href="icon.png" type="image/png">
|
|
|
|
<style>
|
|
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 400; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-400-normal.woff2) format('woff2'); }
|
|
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 500; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-500-normal.woff2) format('woff2'); }
|
|
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 600; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-600-normal.woff2) format('woff2'); }
|
|
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 700; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-700-normal.woff2) format('woff2'); }
|
|
@font-face { font-family: 'Google Sans Flex'; font-style: normal; font-display: swap; font-weight: 800; src: url(https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex@latest/latin-800-normal.woff2) format('woff2'); }
|
|
</style>
|
|
|
|
<style>
|
|
:root {
|
|
--green: #1DB954;
|
|
--green-dim: #1aa34a;
|
|
--bg: #0a0a0a;
|
|
--bg-card: #1a1a1a;
|
|
--bg-card-hover: #222222;
|
|
--surface: #121212;
|
|
--text: #e8e8e8;
|
|
--text-dim: #999;
|
|
--max-w: 900px;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html { scroll-behavior: smooth; scrollbar-width: thin; scrollbar-color: #333 transparent; }
|
|
html::-webkit-scrollbar { width: 8px; }
|
|
html::-webkit-scrollbar-track { background: transparent; }
|
|
html::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
|
html::-webkit-scrollbar-thumb:hover { background: #555; }
|
|
|
|
body {
|
|
font-family: 'Google Sans Flex', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
background: var(--bg); color: var(--text); line-height: 1.6;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
a { color: var(--green); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
|
|
nav {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
|
background: rgba(18,18,18,.78);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
}
|
|
.nav-inner {
|
|
max-width: 1100px; margin: auto;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0 24px; height: 64px;
|
|
}
|
|
.nav-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 1.1rem; color: var(--text); }
|
|
.nav-brand img { width: 32px; height: 32px; border-radius: 50%; }
|
|
.nav-links { display: flex; align-items: center; gap: 24px; list-style: none; }
|
|
.nav-links a { color: var(--text-dim); font-size: .9rem; transition: color .2s; }
|
|
.nav-links a:hover { color: var(--text); text-decoration: none; }
|
|
.nav-links a.active { color: var(--text); font-weight: 600; }
|
|
.nav-links .nav-icon { display: flex; align-items: center; opacity: .6; transition: opacity .2s; margin-left: -12px; }
|
|
.nav-links .nav-icon:hover { opacity: 1; }
|
|
.nav-links .nav-icon svg { width: 24px; height: 24px; fill: currentColor; }
|
|
.nav-links .nav-divider { width: 1px; height: 20px; background: rgba(255,255,255,.15); margin-left: -4px; }
|
|
.search-trigger {
|
|
display: flex; align-items: center; gap: 6px;
|
|
background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.12);
|
|
border-radius: 8px; padding: 6px 12px;
|
|
color: var(--text-dim); font-size: .85rem; cursor: pointer;
|
|
font-family: inherit; transition: color .2s, border-color .2s, background .2s;
|
|
white-space: nowrap; text-decoration: none;
|
|
}
|
|
.search-trigger:hover { color: var(--text); border-color: rgba(255,255,255,.25); background: rgba(255,255,255,.1); text-decoration: none; }
|
|
.search-trigger svg { width: 14px; height: 14px; fill: currentColor; flex-shrink: 0; }
|
|
.search-trigger kbd {
|
|
background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.1);
|
|
border-radius: 4px; padding: 0px 4px; font-size: .65rem;
|
|
font-family: inherit; color: #555; line-height: 1.4; margin-left: 2px;
|
|
}
|
|
|
|
.page-header {
|
|
padding: 100px 24px 40px; text-align: center;
|
|
}
|
|
.page-header h1 { font-size: 2rem; font-weight: 800; margin-bottom: 8px; }
|
|
.page-header p { color: var(--text-dim); font-size: 1rem; }
|
|
|
|
.latest-hero {
|
|
max-width: var(--max-w); margin: 0 auto; padding: 0 24px 40px;
|
|
}
|
|
.latest-card {
|
|
background: var(--bg-card-hover); border-radius: 20px;
|
|
padding: 32px; position: relative; overflow: hidden;
|
|
}
|
|
.latest-header {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;
|
|
}
|
|
.latest-tag { font-size: 1.6rem; font-weight: 800; }
|
|
.latest-badge {
|
|
font-size: .7rem; font-weight: 700; text-transform: uppercase;
|
|
padding: 4px 12px; border-radius: 999px;
|
|
background: var(--green); color: #000;
|
|
}
|
|
.latest-date { font-size: .85rem; color: var(--text-dim); margin-left: auto; }
|
|
.latest-assets {
|
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 10px; margin-bottom: 24px;
|
|
}
|
|
.latest-asset {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 14px 18px; border-radius: 16px;
|
|
background: rgba(29,185,84,.08);
|
|
color: var(--text); transition: background .2s; text-decoration: none;
|
|
}
|
|
.latest-asset:hover { background: rgba(29,185,84,.15); text-decoration: none; }
|
|
.latest-asset-icon { color: var(--green); flex-shrink: 0; }
|
|
.latest-asset-info { min-width: 0; }
|
|
.latest-asset-name { font-size: .85rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.latest-asset-meta { font-size: .75rem; color: var(--text-dim); }
|
|
.latest-changelog-toggle {
|
|
background: var(--bg-card); border: none; border-radius: 16px;
|
|
color: var(--text-dim); padding: 10px 16px; font-size: .85rem;
|
|
cursor: pointer; transition: background .2s; width: 100%;
|
|
font-family: inherit;
|
|
}
|
|
.latest-changelog-toggle:hover { background: var(--surface); color: var(--text); }
|
|
.latest-changelog {
|
|
display: none; margin-top: 16px; padding-top: 16px;
|
|
border-top: 1px solid rgba(255,255,255,.06);
|
|
}
|
|
.latest-changelog.show { display: block; }
|
|
|
|
.older-section {
|
|
max-width: var(--max-w); margin: 0 auto; padding: 40px 24px 80px;
|
|
}
|
|
.older-title {
|
|
font-size: 1.1rem; font-weight: 600; color: var(--text-dim);
|
|
margin-bottom: 16px; padding-bottom: 12px;
|
|
}
|
|
|
|
.release-card {
|
|
background: var(--bg-card); border-radius: 16px;
|
|
margin-bottom: 8px; transition: background .2s;
|
|
}
|
|
.release-card:hover { background: var(--bg-card-hover); }
|
|
.release-summary {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
padding: 16px 20px; cursor: pointer; list-style: none;
|
|
}
|
|
.release-summary::-webkit-details-marker { display: none; }
|
|
.release-tag { font-size: 1rem; font-weight: 700; }
|
|
.release-badge {
|
|
font-size: .65rem; font-weight: 700; text-transform: uppercase;
|
|
padding: 2px 8px; border-radius: 999px;
|
|
}
|
|
.release-badge-pre { background: #f59e0b; color: #000; }
|
|
.release-date { font-size: .8rem; color: var(--text-dim); margin-left: auto; }
|
|
.release-expand { color: var(--text-dim); font-size: .8rem; transition: transform .2s; }
|
|
details[open] .release-expand { transform: rotate(180deg); }
|
|
.release-detail { padding: 0 20px 20px; }
|
|
.release-assets { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
|
.release-asset {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 7px 14px; border-radius: 12px; font-size: .82rem; font-weight: 500;
|
|
background: rgba(29,185,84,.08);
|
|
color: var(--green); transition: background .2s; text-decoration: none;
|
|
}
|
|
.release-asset:hover { background: rgba(29,185,84,.15); text-decoration: none; }
|
|
.release-asset-size { color: var(--text-dim); font-size: .72rem; }
|
|
|
|
.release-body {
|
|
font-size: .85rem; color: var(--text-dim); line-height: 1.7;
|
|
max-height: 400px; overflow-y: auto;
|
|
scrollbar-width: thin; scrollbar-color: #333 transparent;
|
|
}
|
|
.release-body h1, .release-body h2, .release-body h3 {
|
|
color: var(--text); font-size: .95rem; margin: 16px 0 8px;
|
|
}
|
|
.release-body h1:first-child, .release-body h2:first-child, .release-body h3:first-child { margin-top: 0; }
|
|
.release-body ul { padding-left: 20px; margin: 4px 0; }
|
|
.release-body li { margin: 4px 0; }
|
|
.release-body code { background: var(--bg-card-hover); padding: 2px 6px; border-radius: 4px; font-size: .8rem; }
|
|
.release-body a { color: var(--green); }
|
|
|
|
footer {
|
|
background: var(--surface);
|
|
padding: 40px 24px; text-align: center;
|
|
}
|
|
.footer-inner { max-width: 1100px; margin: auto; }
|
|
.footer-links { display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; margin-bottom: 16px; }
|
|
.footer-links a { color: var(--text-dim); font-size: .9rem; }
|
|
.footer-links a:hover { color: var(--text); }
|
|
.footer-copy { color: #555; font-size: .8rem; }
|
|
|
|
.loading { text-align: center; color: var(--text-dim); padding: 60px 0; }
|
|
.loading-spinner {
|
|
width: 32px; height: 32px; margin: 0 auto 12px;
|
|
border: 3px solid var(--surface); border-top-color: var(--green);
|
|
border-radius: 50%; animation: spin .8s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.all-releases-link {
|
|
display: block; text-align: center; padding: 24px;
|
|
color: var(--text-dim); font-size: .9rem;
|
|
}
|
|
|
|
.nav-burger {
|
|
display: none; width: 40px; height: 40px; border-radius: 12px;
|
|
background: none; border: none; cursor: pointer;
|
|
align-items: center; justify-content: center; flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
.nav-burger .bar {
|
|
display: block; width: 20px; height: 2px; background: var(--text);
|
|
border-radius: 2px; transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .2s;
|
|
position: absolute; left: 10px;
|
|
}
|
|
.nav-burger .bar:nth-child(1) { top: 12px; }
|
|
.nav-burger .bar:nth-child(2) { top: 19px; }
|
|
.nav-burger .bar:nth-child(3) { top: 26px; }
|
|
.nav-burger.active .bar:nth-child(1) { top: 19px; transform: rotate(45deg); }
|
|
.nav-burger.active .bar:nth-child(2) { opacity: 0; }
|
|
.nav-burger.active .bar:nth-child(3) { top: 19px; transform: rotate(-45deg); }
|
|
.mobile-overlay {
|
|
position: fixed; top: 64px; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,.5); z-index: 98;
|
|
opacity: 0; pointer-events: none;
|
|
transition: opacity .3s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
.mobile-overlay.open { opacity: 1; pointer-events: auto; }
|
|
.mobile-menu {
|
|
position: fixed; top: 64px; left: 0; right: 0;
|
|
background: rgba(18,18,18,.95); padding: 8px 16px 16px; z-index: 99;
|
|
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
|
transform: translateY(-8px); opacity: 0; pointer-events: none;
|
|
transition: transform .3s cubic-bezier(.4,0,.2,1), opacity .3s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
.mobile-menu.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
|
|
.mobile-menu a {
|
|
display: flex; align-items: center; gap: 12px;
|
|
padding: 14px 16px; border-radius: 12px;
|
|
color: var(--text-dim); font-size: .95rem; font-weight: 500;
|
|
transition: background .2s; opacity: 0; transform: translateY(-6px);
|
|
}
|
|
.mobile-menu.open a {
|
|
opacity: 1; transform: translateY(0);
|
|
transition: background .2s, opacity .3s cubic-bezier(.4,0,.2,1), transform .3s cubic-bezier(.4,0,.2,1);
|
|
}
|
|
.mobile-menu.open a:nth-child(1) { transition-delay: .03s; }
|
|
.mobile-menu.open a:nth-child(2) { transition-delay: .06s; }
|
|
.mobile-menu.open a:nth-child(3) { transition-delay: .09s; }
|
|
.mobile-menu.open a:nth-child(4) { transition-delay: .12s; }
|
|
.mobile-menu.open a:nth-child(5) { transition-delay: .15s; }
|
|
.mobile-menu a:hover { background: var(--bg-card); color: var(--text); text-decoration: none; }
|
|
.mobile-menu a.active { color: var(--text); font-weight: 600; background: var(--bg-card); }
|
|
.mobile-menu .mobile-divider {
|
|
height: 1px; background: rgba(255,255,255,.06); margin: 4px 0;
|
|
opacity: 0; transition: opacity .3s .15s;
|
|
}
|
|
.mobile-menu.open .mobile-divider { opacity: 1; }
|
|
.mobile-menu .mobile-icons {
|
|
display: flex; gap: 8px; padding: 8px 16px 0;
|
|
opacity: 0; transform: translateY(-6px);
|
|
transition: opacity .3s cubic-bezier(.4,0,.2,1) .18s, transform .3s cubic-bezier(.4,0,.2,1) .18s;
|
|
}
|
|
.mobile-menu.open .mobile-icons { opacity: 1; transform: translateY(0); }
|
|
.mobile-menu .mobile-icons a {
|
|
padding: 10px; border-radius: 12px; background: var(--bg-card);
|
|
display: flex; align-items: center; justify-content: center;
|
|
opacity: 1; transform: none;
|
|
}
|
|
.mobile-menu .mobile-icons a svg { width: 20px; height: 20px; fill: currentColor; }
|
|
|
|
@media (max-width: 640px) {
|
|
.nav-links { display: none; }
|
|
.nav-burger { display: flex; }
|
|
.page-header { padding: 80px 16px 32px; }
|
|
.latest-hero { padding: 0 16px 32px; }
|
|
.latest-card { padding: 20px; }
|
|
.latest-header { flex-direction: column; align-items: flex-start; gap: 6px; }
|
|
.latest-date { margin-left: 0; }
|
|
.latest-assets { grid-template-columns: 1fr; }
|
|
.older-section { padding: 32px 16px 60px; }
|
|
.release-summary { flex-direction: row; gap: 8px; }
|
|
.release-date { margin-left: auto; }
|
|
}
|
|
|
|
.icon-svg { width: 20px; height: 20px; fill: currentColor; }
|
|
|
|
.search-overlay {
|
|
position: fixed; inset: 0; background: rgba(0,0,0,.6);
|
|
z-index: 300; opacity: 0; pointer-events: none;
|
|
transition: opacity .2s cubic-bezier(.4,0,.2,1);
|
|
display: flex; align-items: flex-start; justify-content: center;
|
|
padding-top: min(20vh, 140px);
|
|
}
|
|
.search-overlay.open { opacity: 1; pointer-events: auto; }
|
|
.search-modal {
|
|
background: var(--surface); border: 1px solid rgba(255,255,255,.1);
|
|
border-radius: 16px; width: 580px; max-width: calc(100vw - 32px);
|
|
max-height: min(70vh, 520px); display: flex; flex-direction: column;
|
|
box-shadow: 0 16px 70px rgba(0,0,0,.6);
|
|
transform: translateY(-12px) scale(.97); opacity: 0;
|
|
transition: transform .25s cubic-bezier(.4,0,.2,1), opacity .2s;
|
|
}
|
|
.search-overlay.open .search-modal { transform: translateY(0) scale(1); opacity: 1; }
|
|
.search-header {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,.08);
|
|
}
|
|
.search-header svg { width: 18px; height: 18px; fill: var(--text-dim); flex-shrink: 0; }
|
|
.search-input {
|
|
flex: 1; background: none; border: none; outline: none;
|
|
color: var(--text); font-size: .95rem; font-family: inherit;
|
|
}
|
|
.search-input::placeholder { color: var(--text-dim); }
|
|
.search-esc {
|
|
background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.1);
|
|
border-radius: 4px; padding: 2px 7px; font-size: .72rem;
|
|
color: var(--text-dim); font-family: inherit; cursor: pointer;
|
|
}
|
|
.search-esc:hover { background: rgba(255,255,255,.14); }
|
|
.search-body { overflow-y: auto; padding: 8px; scrollbar-width: thin; scrollbar-color: #333 transparent; }
|
|
.search-body::-webkit-scrollbar { width: 6px; }
|
|
.search-body::-webkit-scrollbar-track { background: transparent; }
|
|
.search-body::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
|
.search-group-label {
|
|
padding: 8px 10px 4px; font-size: .72rem; font-weight: 600;
|
|
color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em;
|
|
}
|
|
.search-item {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 10px 12px; border-radius: 10px; cursor: pointer;
|
|
color: var(--text); font-size: .88rem; transition: background .15s;
|
|
}
|
|
.search-item:hover, .search-item.active { background: rgba(255,255,255,.07); }
|
|
.search-item svg { width: 16px; height: 16px; fill: var(--text-dim); flex-shrink: 0; }
|
|
.search-item-text { flex: 1; min-width: 0; }
|
|
.search-item-title { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.search-item-section { font-size: .78rem; color: var(--text-dim); margin-top: 1px; }
|
|
.search-item mark { background: rgba(29,185,84,.25); color: var(--text); border-radius: 2px; padding: 0 1px; }
|
|
.search-item .search-enter { color: var(--text-dim); font-size: .72rem; opacity: 0; transition: opacity .15s; }
|
|
.search-item.active .search-enter { opacity: 1; }
|
|
.search-empty { padding: 32px 16px; text-align: center; color: var(--text-dim); font-size: .9rem; }
|
|
.search-footer {
|
|
padding: 10px 16px; border-top: 1px solid rgba(255,255,255,.06);
|
|
display: flex; align-items: center; gap: 16px; font-size: .72rem; color: #555;
|
|
}
|
|
.search-footer kbd {
|
|
background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.08);
|
|
border-radius: 3px; padding: 1px 4px; font-family: inherit; font-size: .68rem;
|
|
}
|
|
@media (max-width: 640px) {
|
|
.search-trigger kbd { display: none; }
|
|
.search-overlay { padding-top: 16px; }
|
|
.search-modal { max-height: 80vh; border-radius: 14px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<nav>
|
|
<div class="nav-inner">
|
|
<a class="nav-brand" href="index">
|
|
<img src="icon.png" alt="SpotiFLAC">
|
|
SpotiFLAC Mobile
|
|
</a>
|
|
<ul class="nav-links">
|
|
<li><a href="index#features">Features</a></li>
|
|
<li><a href="downloads" class="active">Downloads</a></li>
|
|
<li><a href="index#faq">FAQ</a></li>
|
|
<li><a href="partners">Partners</a></li>
|
|
<li><a href="docs">Docs</a></li>
|
|
<li class="nav-divider"></li>
|
|
<li><button class="search-trigger" onclick="openSearch()" aria-label="Search documentation">
|
|
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
|
Search
|
|
<kbd id="searchShortcutHint">Ctrl K</kbd>
|
|
</button></li>
|
|
<li><a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" class="nav-icon" aria-label="GitHub"><svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.82-.26.82-.57L9 20.86c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6.02 0c2.28-1.55 3.29-1.23 3.29-1.23.66 1.66.25 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22l-.01 3.29c0 .31.21.69.82.57A12 12 0 0 0 12 .3"/></svg></a></li>
|
|
<li><a href="https://t.me/spotiflac" target="_blank" class="nav-icon" aria-label="Telegram"><svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg></a></li>
|
|
</ul>
|
|
<button class="nav-burger" onclick="toggleMenu()" aria-label="Menu">
|
|
<span class="bar"></span><span class="bar"></span><span class="bar"></span>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="mobile-overlay" id="mobileOverlay" onclick="toggleMenu()"></div>
|
|
<div class="mobile-menu" id="mobileMenu">
|
|
<a href="index#features">Features</a>
|
|
<a href="downloads" class="active">Downloads</a>
|
|
<a href="index#faq">FAQ</a>
|
|
<a href="partners">Partners</a>
|
|
<a href="docs">Docs</a>
|
|
<a href="javascript:void(0)" onclick="toggleMenu();openSearch()" style="display:flex;align-items:center;gap:6px;color:var(--text-dim)"><svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor"><path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>Search Docs</a>
|
|
<div class="mobile-divider"></div>
|
|
<div class="mobile-icons">
|
|
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank" aria-label="GitHub">
|
|
<svg viewBox="0 0 24 24"><path d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.5-1.4-1.3-1.8-1.3-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.3.5-2.3 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z"/></svg>
|
|
</a>
|
|
<a href="https://t.me/spotiflac" target="_blank" aria-label="Telegram">
|
|
<svg viewBox="0 0 24 24"><path d="M11.94 24c6.6 0 12-5.4 12-12s-5.4-12-12-12-12 5.4-12 12 5.4 12 12 12zm-3.2-8.69l-.37-3.04 8.52-5.18c.38-.23.73.09.45.35l-6.96 6.4-.29 2.97c-.04.35-.48.43-.64.12l-1.64-3.33-3.6-1.17c-.78-.24-.8-.78-.02-1.14l14.04-5.4c.65-.25 1.25.15 1.04.83l-2.39 11.28c-.18.81-.7 1.01-1.42.63l-3.92-2.89-1.89 1.82c-.21.2-.39.38-.65.38l.28-3.06z"/></svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="page-header">
|
|
<h1>Downloads</h1>
|
|
<p>Latest releases with changelog and direct download links.</p>
|
|
</div>
|
|
|
|
<div class="latest-hero" id="latest-hero">
|
|
<div class="loading">
|
|
<div class="loading-spinner"></div>
|
|
Loading latest release...
|
|
</div>
|
|
</div>
|
|
|
|
<div class="older-section" id="older-section" style="display:none">
|
|
<div class="older-title">Previous Releases</div>
|
|
<div id="older-releases"></div>
|
|
<a class="all-releases-link" href="https://github.com/zarzet/SpotiFLAC-Mobile/releases" target="_blank">
|
|
View all releases on GitHub →
|
|
</a>
|
|
</div>
|
|
|
|
<div class="search-overlay" id="searchOverlay" onclick="if(event.target===this)closeSearch()">
|
|
<div class="search-modal">
|
|
<div class="search-header">
|
|
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
|
<input class="search-input" id="searchInput" type="text" placeholder="Search documentation..." autocomplete="off" spellcheck="false">
|
|
<button class="search-esc" onclick="closeSearch()">Esc</button>
|
|
</div>
|
|
<div class="search-body" id="searchBody">
|
|
<div class="search-empty">Type to search across all documentation sections</div>
|
|
</div>
|
|
<div class="search-footer">
|
|
<span><kbd>↑</kbd> <kbd>↓</kbd> to navigate</span>
|
|
<span><kbd>Enter</kbd> to select</span>
|
|
<span><kbd>Esc</kbd> to close</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<div class="footer-inner">
|
|
<div class="footer-links">
|
|
<a href="index">Home</a>
|
|
<a href="docs">Documentation</a>
|
|
<a href="https://github.com/zarzet/SpotiFLAC-Mobile" target="_blank">GitHub</a>
|
|
<a href="https://t.me/spotiflac" target="_blank">Telegram</a>
|
|
<a href="https://ko-fi.com/zarzet" target="_blank">Support / Ko-fi</a>
|
|
</div>
|
|
<p class="footer-copy">SpotiFLAC is for educational and private use only. Not affiliated with any streaming service.</p>
|
|
<p class="footer-copy">© 2026 SpotiFLAC · Open Source · <a href="https://opensource.org/license/mit" target="_blank" style="color:inherit;text-decoration:underline">MIT Licensed</a></p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
(async () => {
|
|
const REPO = 'zarzet/SpotiFLAC-Mobile';
|
|
const latestEl = document.getElementById('latest-hero');
|
|
const olderEl = document.getElementById('older-releases');
|
|
const olderSection = document.getElementById('older-section');
|
|
|
|
try {
|
|
const res = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=10`);
|
|
if (!res.ok) throw new Error(res.status);
|
|
const releases = await res.json();
|
|
if (!releases.length) { latestEl.innerHTML = '<p style="text-align:center;color:#999;padding:40px">No releases found.</p>'; return; }
|
|
|
|
// Latest release
|
|
const latest = releases[0];
|
|
const latestDate = fmtDate(latest.published_at);
|
|
const latestBody = md(latest.body || '');
|
|
const latestAssets = (latest.assets || []).filter(a => !a.name.endsWith('.sha256'));
|
|
|
|
latestEl.innerHTML = `
|
|
<div class="latest-card">
|
|
<div class="latest-header">
|
|
<span class="latest-tag">${latest.tag_name}</span>
|
|
<span class="latest-badge">${latest.prerelease ? 'Pre-release' : 'Latest Release'}</span>
|
|
<span class="latest-date">${latestDate}</span>
|
|
</div>
|
|
<div class="latest-assets">
|
|
${latestAssets.map(a => `
|
|
<a class="latest-asset" href="${a.browser_download_url}">
|
|
<svg class="latest-asset-icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
|
</svg>
|
|
<div class="latest-asset-info">
|
|
<div class="latest-asset-name">${a.name}</div>
|
|
<div class="latest-asset-meta">${fmtSize(a.size)} · ${fmtCount(a.download_count)} downloads</div>
|
|
</div>
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
${latestBody ? `
|
|
<button class="latest-changelog-toggle" onclick="this.nextElementSibling.classList.toggle('show'); this.textContent = this.nextElementSibling.classList.contains('show') ? 'Hide changelog' : 'Show changelog'">
|
|
Show changelog
|
|
</button>
|
|
<div class="latest-changelog">
|
|
<div class="release-body">${latestBody}</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
|
|
// Older releases
|
|
const older = releases.slice(1);
|
|
if (older.length) {
|
|
olderSection.style.display = '';
|
|
olderEl.innerHTML = older.map(r => {
|
|
const date = fmtDate(r.published_at);
|
|
const body = md(r.body || '');
|
|
const assets = (r.assets || []).filter(a => !a.name.endsWith('.sha256'));
|
|
|
|
return `
|
|
<details class="release-card">
|
|
<summary class="release-summary">
|
|
<span class="release-tag">${r.tag_name}</span>
|
|
${r.prerelease ? '<span class="release-badge release-badge-pre">Pre-release</span>' : ''}
|
|
<span class="release-date">${date}</span>
|
|
<span class="release-expand">▼</span>
|
|
</summary>
|
|
<div class="release-detail">
|
|
${assets.length ? `
|
|
<div class="release-assets">
|
|
${assets.map(a => `
|
|
<a class="release-asset" href="${a.browser_download_url}" target="_blank">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
|
${a.name}
|
|
<span class="release-asset-size">${fmtSize(a.size)}</span>
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
` : ''}
|
|
${body ? `<div class="release-body">${body}</div>` : ''}
|
|
</div>
|
|
</details>
|
|
`;
|
|
}).join('');
|
|
}
|
|
} catch (e) {
|
|
latestEl.innerHTML = `<p style="text-align:center;color:#999;padding:40px">Failed to load releases. <a href="https://github.com/${REPO}/releases" target="_blank">View on GitHub</a></p>`;
|
|
}
|
|
|
|
function fmtDate(d) { return new Date(d).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' }); }
|
|
function fmtSize(b) { return b < 1048576 ? (b/1024).toFixed(1)+' KB' : (b/1048576).toFixed(1)+' MB'; }
|
|
function fmtCount(n) { return n >= 1000 ? (n/1000).toFixed(1)+'k' : n; }
|
|
function md(s) {
|
|
return s
|
|
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>')
|
|
.replace(/^[-*] (.+)$/gm, '<li>$1</li>')
|
|
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
|
.replace(/\n{2,}/g, '<br>')
|
|
.replace(/(^<ul>)|(<\/ul>$)/g, '')
|
|
.replace(/(<li>[\s\S]*?<\/li>)(?=\s*<h|$|\s*<br>)/g, '<ul>$1</ul>');
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
function toggleMenu() {
|
|
document.getElementById('mobileMenu').classList.toggle('open');
|
|
document.getElementById('mobileOverlay').classList.toggle('open');
|
|
document.querySelector('.nav-burger').classList.toggle('active');
|
|
}
|
|
document.getElementById('mobileMenu').addEventListener('click', function(e) {
|
|
if (e.target.closest('a')) toggleMenu();
|
|
});
|
|
</script>
|
|
<script>
|
|
(function() {
|
|
var overlay = document.getElementById('searchOverlay');
|
|
var input = document.getElementById('searchInput');
|
|
var body = document.getElementById('searchBody');
|
|
var hintEl = document.getElementById('searchShortcutHint');
|
|
var activeIdx = -1;
|
|
var results = [];
|
|
|
|
if (navigator.platform && navigator.platform.indexOf('Mac') > -1) {
|
|
if (hintEl) hintEl.textContent = '\u2318 K';
|
|
}
|
|
|
|
var searchIndex = [{"id":"table-of-contents","title":"Table of Contents","level":2,"section":"Table of Contents"},{"id":"introduction","title":"Introduction","level":2,"section":"Introduction"},{"id":"requirements","title":"Requirements","level":3,"section":"Introduction"},{"id":"extension-structure","title":"Extension Structure","level":2,"section":"Extension Structure"},{"id":"manifest-file","title":"Manifest File","level":2,"section":"Manifest File"},{"id":"complete-manifest-example","title":"Complete Manifest Example","level":3,"section":"Manifest File"},{"id":"manifest-fields","title":"Manifest Fields","level":3,"section":"Manifest File"},{"id":"quality-options","title":"Quality Options","level":3,"section":"Manifest File"},{"id":"quality-specific-settings","title":"Quality-Specific Settings","level":3,"section":"Manifest File"},{"id":"permissions","title":"Permissions","level":3,"section":"Manifest File"},{"id":"extension-types","title":"Extension Types","level":3,"section":"Manifest File"},{"id":"settings","title":"Settings","level":3,"section":"Manifest File"},{"id":"button-setting-type","title":"Button Setting Type","level":3,"section":"Manifest File"},{"id":"custom-search-behavior","title":"Custom Search Behavior","level":3,"section":"Manifest File"},{"id":"thumbnail-ratio-presets","title":"Thumbnail Ratio Presets","level":4,"section":"Manifest File"},{"id":"custom-url-handler","title":"Custom URL Handler","level":3,"section":"Manifest File"},{"id":"album--playlist-functions-v301","title":"Album & Playlist Functions (v3.0.1+)","level":3,"section":"Manifest File"},{"id":"artist-support","title":"Artist Support","level":3,"section":"Manifest File"},{"id":"home-feed-support","title":"Home Feed Support","level":3,"section":"Manifest File"},{"id":"track-enrichment","title":"Track Enrichment","level":3,"section":"Manifest File"},{"id":"custom-track-matching","title":"Custom Track Matching","level":3,"section":"Manifest File"},{"id":"post-processing-hooks","title":"Post-Processing Hooks","level":3,"section":"Manifest File"},{"id":"post-process-api-v2-recommended","title":"Post-Process API v2 (Recommended)","level":4,"section":"Manifest File"},{"id":"main-script","title":"Main Script","level":2,"section":"Main Script"},{"id":"basic-structure","title":"Basic Structure","level":3,"section":"Main Script"},{"id":"important-registerextension","title":"Important: registerExtension()","level":3,"section":"Main Script"},{"id":"api-reference","title":"API Reference","level":2,"section":"API Reference"},{"id":"http-api","title":"HTTP API","level":3,"section":"API Reference"},{"id":"request-headers","title":"Request Headers","level":4,"section":"API Reference"},{"id":"response-object","title":"Response Object","level":4,"section":"API Reference"},{"id":"form-encoded-post-applicationx-www-form-urlencoded","title":"Form-Encoded POST (application/x-www-form-urlencoded)","level":4,"section":"API Reference"},{"id":"cookie-jar","title":"Cookie Jar","level":4,"section":"API Reference"},{"id":"youtube-music--innertube-api-example","title":"YouTube Music / Innertube API Example","level":4,"section":"API Reference"},{"id":"browser-like-polyfills","title":"Browser-like Polyfills","level":3,"section":"API Reference"},{"id":"fetch-api","title":"fetch() API","level":4,"section":"API Reference"},{"id":"atob--btoa","title":"atob() / btoa()","level":4,"section":"API Reference"},{"id":"textencoder--textdecoder","title":"TextEncoder / TextDecoder","level":4,"section":"API Reference"},{"id":"url--urlsearchparams","title":"URL / URLSearchParams","level":4,"section":"API Reference"},{"id":"porting-browser-libraries","title":"Porting Browser Libraries","level":4,"section":"API Reference"},{"id":"storage-api","title":"Storage API","level":3,"section":"API Reference"},{"id":"file-api","title":"File API","level":3,"section":"API Reference"},{"id":"logging-api","title":"Logging API","level":3,"section":"API Reference"},{"id":"utility-api","title":"Utility API","level":3,"section":"API Reference"},{"id":"hmac-sha1-for-totp","title":"HMAC-SHA1 for TOTP","level":4,"section":"API Reference"},{"id":"hmac-sha256-example-api-signing","title":"HMAC-SHA256 Example (API Signing)","level":4,"section":"API Reference"},{"id":"go-backend-api","title":"Go Backend API","level":3,"section":"API Reference"},{"id":"using-getlocaltime-for-time-based-greeting","title":"Using getLocalTime() for Time-Based Greeting","level":4,"section":"API Reference"},{"id":"using-getlocaltime-for-timezone-in-api-calls","title":"Using getLocalTime() for Timezone in API Calls","level":4,"section":"API Reference"},{"id":"credentials-api-encrypted","title":"Credentials API (Encrypted)","level":3,"section":"API Reference"},{"id":"auth-api-oauth-support","title":"Auth API (OAuth Support)","level":3,"section":"API Reference"},{"id":"pkce-oauth-flow-recommended","title":"PKCE OAuth Flow (Recommended)","level":3,"section":"API Reference"},{"id":"quick-start-high-level-api","title":"Quick Start (High-Level API)","level":4,"section":"API Reference"},{"id":"low-level-api-manual-control","title":"Low-Level API (Manual Control)","level":4,"section":"API Reference"},{"id":"pkce-api-reference","title":"PKCE API Reference","level":4,"section":"API Reference"},{"id":"complete-oauth-example","title":"Complete OAuth Example","level":4,"section":"API Reference"},{"id":"crypto-utilities","title":"Crypto Utilities","level":3,"section":"API Reference"},{"id":"ffmpeg-api-post-processing","title":"FFmpeg API (Post-Processing)","level":3,"section":"API Reference"},{"id":"track-matching-api","title":"Track Matching API","level":3,"section":"API Reference"},{"id":"extension-examples","title":"Extension Examples","level":2,"section":"Extension Examples"},{"id":"example-1-simple-metadata-provider","title":"Example 1: Simple Metadata Provider","level":3,"section":"Extension Examples"},{"id":"example-2-download-provider-with-auth","title":"Example 2: Download Provider with Auth","level":3,"section":"Extension Examples"},{"id":"packaging--distribution","title":"Packaging & Distribution","level":2,"section":"Packaging & Distribution"},{"id":"project-structure","title":"Project Structure","level":3,"section":"Packaging & Distribution"},{"id":"module-system-limitation","title":"Module System Limitation","level":3,"section":"Packaging & Distribution"},{"id":"creating-extension-file","title":"Creating Extension File","level":3,"section":"Packaging & Distribution"},{"id":"installing-extension","title":"Installing Extension","level":3,"section":"Packaging & Distribution"},{"id":"upgrading-extension","title":"Upgrading Extension","level":3,"section":"Packaging & Distribution"},{"id":"troubleshooting","title":"Troubleshooting","level":2,"section":"Troubleshooting"},{"id":"error-extension-did-not-call-registerextension","title":"Error: extension did not call registerExtension()","level":3,"section":"Troubleshooting"},{"id":"error-permission-denied-for-domain-x--network-access-denied","title":"Error: Permission denied for domain X / network access denied","level":3,"section":"Troubleshooting"},{"id":"error-post-body-is-object-object","title":"Error: POST body is [object Object]","level":3,"section":"Troubleshooting"},{"id":"error-function-x-is-not-defined","title":"Error: Function X is not defined","level":3,"section":"Troubleshooting"},{"id":"error-invalid-manifest","title":"Error: Invalid manifest","level":3,"section":"Troubleshooting"},{"id":"extension-doesnt-appear-after-install","title":"Extension doesn't appear after install","level":3,"section":"Troubleshooting"},{"id":"http-request-fails","title":"HTTP request fails","level":3,"section":"Troubleshooting"},{"id":"download-fails","title":"Download fails","level":3,"section":"Troubleshooting"},{"id":"error-file-access-denied-extension-does-not-have-file-permission","title":"Error: file access denied: extension does not have file permission","level":3,"section":"Troubleshooting"},{"id":"error-file-access-denied-absolute-paths-are-not-allowed","title":"Error: file access denied: absolute paths are not allowed","level":3,"section":"Troubleshooting"},{"id":"error-file-access-denied-path-x-is-outside-sandbox","title":"Error: file access denied: path X is outside sandbox","level":3,"section":"Troubleshooting"},{"id":"error-cannot-downgrade-extension","title":"Error: Cannot downgrade extension","level":3,"section":"Troubleshooting"},{"id":"error-extension-is-already-installed","title":"Error: Extension is already installed","level":3,"section":"Troubleshooting"},{"id":"error-timeout-extension-took-too-long-to-respond","title":"Error: timeout: extension took too long to respond","level":3,"section":"Troubleshooting"},{"id":"thumbnails-not-showing-correctly-in-search-results","title":"Thumbnails not showing correctly in search results","level":3,"section":"Troubleshooting"},{"id":"technical-details--behavior","title":"Technical Details & Behavior","level":2,"section":"Technical Details & Behavior"},{"id":"token-refresh-handling","title":"Token Refresh Handling","level":3,"section":"Technical Details & Behavior"},{"id":"storage-limits","title":"Storage Limits","level":3,"section":"Technical Details & Behavior"},{"id":"file-api-path-resolution","title":"File API Path Resolution","level":3,"section":"Technical Details & Behavior"},{"id":"http-redirect-handling","title":"HTTP Redirect Handling","level":3,"section":"Technical Details & Behavior"},{"id":"standard-error-types","title":"Standard Error Types","level":3,"section":"Technical Details & Behavior"},{"id":"http-timeout","title":"HTTP Timeout","level":3,"section":"Technical Details & Behavior"},{"id":"tips--best-practices","title":"Tips & Best Practices","level":2,"section":"Tips & Best Practices"},{"id":"authentication-api","title":"Authentication API","level":2,"section":"Authentication API"},{"id":"auth-api-reference","title":"Auth API Reference","level":3,"section":"Authentication API"},{"id":"credentials-api-encrypted-storage","title":"Credentials API (Encrypted Storage)","level":3,"section":"Authentication API"},{"id":"crypto-utilities-1","title":"Crypto Utilities","level":3,"section":"Authentication API"},{"id":"oauth-flow-example","title":"OAuth Flow Example","level":3,"section":"Authentication API"},{"id":"data-schema-reference","title":"Data Schema Reference","level":2,"section":"Data Schema Reference"},{"id":"track-object","title":"Track Object","level":3,"section":"Data Schema Reference"},{"id":"album-object","title":"Album Object","level":3,"section":"Data Schema Reference"},{"id":"artist-object","title":"Artist Object","level":3,"section":"Data Schema Reference"},{"id":"download-result-object","title":"Download Result Object","level":3,"section":"Data Schema Reference"},{"id":"skip-metadata-enrichment","title":"Skip Metadata Enrichment","level":3,"section":"Data Schema Reference"},{"id":"changelog","title":"Changelog","level":2,"section":"Changelog"},{"id":"support","title":"Support","level":2,"section":"Support"}];
|
|
|
|
function escHtml(s) {
|
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
function highlight(text, query) {
|
|
if (!query) return escHtml(text);
|
|
var escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
var re = new RegExp('(' + escaped + ')', 'gi');
|
|
return escHtml(text).replace(re, '<mark>$1</mark>');
|
|
}
|
|
|
|
function doSearch(query) {
|
|
query = query.trim().toLowerCase();
|
|
if (!query) {
|
|
body.innerHTML = '<div class="search-empty">Type to search across all documentation sections</div>';
|
|
results = []; activeIdx = -1; return;
|
|
}
|
|
var tokens = query.split(/\s+/).filter(Boolean);
|
|
var scored = [];
|
|
searchIndex.forEach(function(item) {
|
|
var titleLow = item.title.toLowerCase();
|
|
var sectionLow = item.section.toLowerCase();
|
|
var score = 0;
|
|
for (var i = 0; i < tokens.length; i++) {
|
|
var t = tokens[i];
|
|
if (titleLow.includes(t)) {
|
|
score += titleLow === t ? 100 : titleLow.startsWith(t) ? 60 : 30;
|
|
} else if (sectionLow.includes(t)) {
|
|
score += 10;
|
|
}
|
|
}
|
|
if (score > 0 && item.level === 2) score += 8;
|
|
if (score > 0 && item.level === 1) score += 15;
|
|
if (score > 0) scored.push({ item: item, score: score });
|
|
});
|
|
scored.sort(function(a, b) { return b.score - a.score; });
|
|
results = scored.slice(0, 20);
|
|
activeIdx = results.length > 0 ? 0 : -1;
|
|
if (!results.length) {
|
|
body.innerHTML = '<div class="search-empty">No results found for "' + escHtml(query) + '"</div>';
|
|
return;
|
|
}
|
|
var hashIcon = '<svg viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>';
|
|
var html = '';
|
|
results.forEach(function(r, i) {
|
|
var cls = i === activeIdx ? ' active' : '';
|
|
var sectionHint = r.item.section && r.item.section !== r.item.title ? r.item.section : '';
|
|
html += '<div class="search-item' + cls + '" data-idx="' + i + '" data-id="' + r.item.id + '">' +
|
|
hashIcon +
|
|
'<div class="search-item-text">' +
|
|
'<div class="search-item-title">' + highlight(r.item.title, query) + '</div>' +
|
|
(sectionHint ? '<div class="search-item-section">' + escHtml(sectionHint) + '</div>' : '') +
|
|
'</div>' +
|
|
'<span class="search-enter">\u21B5</span>' +
|
|
'</div>';
|
|
});
|
|
body.innerHTML = html;
|
|
body.querySelectorAll('.search-item').forEach(function(el) {
|
|
el.addEventListener('click', function() { navigateTo(el.dataset.id); });
|
|
el.addEventListener('mouseenter', function() { setActive(parseInt(el.dataset.idx)); });
|
|
});
|
|
}
|
|
|
|
function setActive(idx) {
|
|
if (idx === activeIdx) return;
|
|
var items = body.querySelectorAll('.search-item');
|
|
if (items[activeIdx]) items[activeIdx].classList.remove('active');
|
|
activeIdx = idx;
|
|
if (items[activeIdx]) {
|
|
items[activeIdx].classList.add('active');
|
|
items[activeIdx].scrollIntoView({ block: 'nearest' });
|
|
}
|
|
}
|
|
|
|
function navigateTo(id) {
|
|
closeSearch();
|
|
window.location.href = 'docs#' + id;
|
|
}
|
|
|
|
window.openSearch = function() {
|
|
overlay.classList.add('open');
|
|
document.body.style.overflow = 'hidden';
|
|
input.value = '';
|
|
doSearch('');
|
|
setTimeout(function() { input.focus(); }, 50);
|
|
};
|
|
|
|
window.closeSearch = function() {
|
|
overlay.classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
activeIdx = -1;
|
|
};
|
|
|
|
input.addEventListener('input', function() { doSearch(input.value); });
|
|
|
|
input.addEventListener('keydown', function(e) {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (results.length) setActive(Math.min(activeIdx + 1, results.length - 1));
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (results.length) setActive(Math.max(activeIdx - 1, 0));
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (results[activeIdx]) navigateTo(results[activeIdx].item.id);
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
closeSearch();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
if (overlay.classList.contains('open')) { closeSearch(); } else { openSearch(); }
|
|
}
|
|
if (e.key === '/' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
|
|
e.preventDefault();
|
|
openSearch();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|