interface PackageIndexEntry { name: string size?: number description?: string website?: string version?: string report?: boolean } function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement { let v = entry.version != null ? `${escapeHtml(entry.version)}` : "" let s = entry.size != null && entry.size > 0 ? `

Size: ${toByteSizeString(entry.size)} (${entry.size})

` : "" let n: string let d: string if ('name_matches' in entry) { n = `

${nameMatches(entry as SearchResult)} ${v}

` } else { n = `

${escapeHtml(entry.name)} ${v}

` } if ('desc_matches' in entry && STATE.getIncludeDescriptions()) { d = descMatches(entry as SearchResult) } else { d = (entry as PackageIndexEntry).description != null ? `

${escapeHtml((entry as PackageIndexEntry).description)}

` : "" } let w = entry.website != null ? `Website` : "" let r = entry.report ? `Log (View | Download)` : "" let row = (document.createElement('tr')) row.innerHTML = ` ${n} ${d} ${s} ${w} ${r} ` return row } function nameMatches(sr: SearchResult): string { return markMatches(sr.name, sr.name_matches) } function descMatches(sr: SearchResult): string { return markMatches(sr.description!, sr.desc_matches) } function markMatches(str: string, indices: [number, number][]): string { if (indices == null) { return str } let out: string = "" let j = 0 for (let i = 0; i < str.length; i++) { if (j < indices.length) { if (i === indices[j][0]) { out += `${escapeHtmlChar(str[i])}` continue } if (i === indices[j][1]) { out += `${escapeHtmlChar(str[i])}` j++ continue } } out += escapeHtmlChar(str[i]) } if (indices[j] !== undefined) { out += "" } return out } function toByteSizeString(bytes: number): string { if (bytes == null) return `unspecified` if (bytes < 1024) return `${bytes}B` if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB` if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB` if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB` if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB` return "not only is it big, it's large" } const API_VERSION = 1 const ENDPOINT = `/api/v${API_VERSION}` interface InfoPayload { count?: number hakurei_version?: string } async function infoRequest(): Promise { const res = await fetch(`${ENDPOINT}/info`) const payload = await res.json() return payload as InfoPayload } interface GetPayload { values?: PackageIndexEntry[] } enum SortOrders { DeclarationAscending, DeclarationDescending, NameAscending, NameDescending } async function getRequest(limit: number, index: number, sort: SortOrders): Promise { const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`) const payload = await res.json() return payload as GetPayload } interface SearchResult extends PackageIndexEntry { name_matches: [number, number][] desc_matches: [number, number][] score: number } interface SearchPayload { count?: number values?: SearchResult[] } async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise { const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`) if (!res.ok) { alert("invalid search query!") exitSearch() return Promise.reject(res.statusText) } const payload = await res.json() return payload as SearchPayload } class State { entriesPerPage: number = 10 entryIndex: number = 0 maxTotal: number = 0 maxEntries: number = 0 sort: SortOrders = SortOrders.DeclarationAscending search: boolean = false getEntriesPerPage(): number { return this.entriesPerPage } setEntriesPerPage(entriesPerPage: number) { this.entriesPerPage = entriesPerPage this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage) } getEntryIndex(): number { return this.entryIndex } setEntryIndex(entryIndex: number) { this.entryIndex = entryIndex this.updatePage() this.updateRange() this.updateListings() } getMaxTotal(): number { return this.maxTotal } setMaxTotal(max: number) { this.maxTotal = max } getSortOrder(): SortOrders { return this.sort } setSortOrder(sortOrder: SortOrders) { this.sort = sortOrder this.setEntryIndex(0) } updatePage() { let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage()) for (let e of document.getElementsByClassName("page-number")) { (e as HTMLInputElement).value = String(page) } } updateRange() { let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal()) document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}` if (this.search) { document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}` document.getElementById("search-query")!.innerHTML = `${escapeHtml(this.getSearchQuery())}` } } getSearchQuery(): string { let queryString = document.getElementById("search")!; return (queryString as HTMLInputElement).value } getIncludeDescriptions(): boolean { let includeDesc = document.getElementById("include-desc")!; return (includeDesc as HTMLInputElement).checked } updateListings() { if (this.search) { searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions()) .then(res => { let table = document.getElementById("pkg-list")! table.innerHTML = '' for (let row of res.values!) { table.appendChild(entryToHTML(row)) } STATE.maxTotal = res.count! STATE.updateRange() if(res.count! < 1) { alert("no results found!") exitSearch() } }) } else { getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder()) .then(res => { let table = document.getElementById("pkg-list")! table.innerHTML = '' for (let row of res.values!) { table.appendChild(entryToHTML(row)) } }) } } } let STATE: State function lastPageIndex(): number { return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage() } function setPage(page: number) { STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex()))) } function escapeHtml(str?: string): string { let out: string = '' if (str == undefined) return "" for (let i = 0; i < str.length; i++) { out += escapeHtmlChar(str[i]) } return out } function escapeHtmlChar(char: string): string { if (char.length != 1) return char switch (char[0]) { case '&': return "&" case '<': return "<" case '>': return ">" case '"': return """ case "'": return "'" default: return char } } function firstPage() { STATE.setEntryIndex(0) } function prevPage() { let index = STATE.getEntryIndex() STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage())) } function lastPage() { STATE.setEntryIndex(lastPageIndex()) } function nextPage() { let index = STATE.getEntryIndex() STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage())) } function doSearch() { document.getElementById("top-controls-regular")!.toggleAttribute("hidden"); document.getElementById("search-top-controls")!.toggleAttribute("hidden"); STATE.search = true; STATE.setEntryIndex(0); } function exitSearch() { document.getElementById("top-controls-regular")!.toggleAttribute("hidden"); document.getElementById("search-top-controls")!.toggleAttribute("hidden"); STATE.search = false; STATE.setMaxTotal(STATE.maxEntries) STATE.setEntryIndex(0) } function main() { STATE = new State() infoRequest() .then(res => { STATE.maxEntries = res.count! STATE.setMaxTotal(STATE.maxEntries) document.getElementById("hakurei-version")!.textContent = res.hakurei_version! STATE.updateRange() STATE.updateListings() }) for (let e of document.getElementsByClassName("page-number")) { e.addEventListener("change", (_) => { setPage(parseInt((e as HTMLInputElement).value)) }) } document.getElementById("count")?.addEventListener("change", (event) => { STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value)) }) document.getElementById("sort")?.addEventListener("change", (event) => { STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value)) }) document.getElementById("search")?.addEventListener("keyup", (event) => { if (event.key === 'Enter') doSearch() }) }