Initial Commit
All checks were successful
ci/woodpecker/release/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful

This commit is contained in:
2025-02-12 19:33:11 -08:00
commit 76cbbad74a
43 changed files with 4970 additions and 0 deletions

28
templates/about.html Normal file
View File

@@ -0,0 +1,28 @@
#macro("content"):
<div class="block">
<p class="is-size-4 mb-1">What is Distrohop?</p>
<p>
Distrohop lets you look up a package from a Linux distro's repositories and find the equivalent package in another distro's repositories.
It also lets you look up a package by its contents. For example, you can look up
<span class="tags has-addons is-inline-block m-0">
<span class="tag is-dark has-background-info-dark has-text-info-light">bin</span><span class="tag is-dark">nano</span>
</span>
and get a list of packages that contain the <code>nano</code> command.
</p>
</div>
<div class="block">
<p class="is-size-4 mb-1">Why is Distrohop?</p>
<p>Distrohop has many use cases. It can be used to figure out which package has the command, library, service, etc. you need. It can also be used to figure out the names of equivalent packages between different distros, which is extremely useful when you're trying to figure out dependencies for packaging software.</p>
</div>
<div class="block">
<p class="is-size-4 mb-1">How is Distrohop?</p>
<p class="mb-2">Distrohop works by downloading and decoding a file index from each supported repo. It analyzes the information contained in the index to form a generalized list of tags describing the contents of each package, and then stores that list in a database.</p>
<p>When you search for a package from another distro, it resolves the package name to its list of tags, and then searches for any packages that match at least one tag in the other distro's repos. It calculates a confidence score based on how many of the tags match, and then sorts the results by confidence.</p>
</div>
<div class="block">
<p class="is-size-4 mb-1">Why is my search so slow?</p>
<p>Each repo can have tens of millions of tags that Distrohop has to churn through. It uses LSM trees and bloom filters to speed the search up as much as possible, and most searches can be measured in milliseconds, but for some searches that contain lots of tags, there may not be any shortcut and Distrohop may have to scan through all or most of the tags stored in the database, which can take a significant amount of time.</p>
</div>
#!macro
#include("base.html", page = "About")

51
templates/base.html Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>#(page | "Unknown") | Distrohop</title>
<link rel="icon" href="/assets/logo/distrohop-no-text.svg">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="/assets/css/style.css">
#macro("?head")
</head>
<body>
<nav x-data="{'active': false}" class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img src="/assets/logo/distrohop.svg" alt="Distrohop Logo">
</a>
<a @click="active = !active" :class="active && 'is-active'" role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" :class="active && 'is-active'">
<div class="navbar-end">
<a class='navbar-item is-tab #(page == "Search" ? "is-active" : "")' href="/">
<div class="icon-text">
<span class="icon is-aligned">#icon("map/search")</span>
<span>Search</span>
</div>
</a>
<a class='navbar-item is-tab #(page == "About" ? "is-active" : "")' href="/about">
<div class="icon-text">
<span class="icon is-aligned">#icon("fe/question")</span>
<span>About</span>
</div>
</a>
</div>
</div>
</nav>
<div class="container mt-5">
#macro("content")
</div>
</body>
</html>

16
templates/error.html Normal file
View File

@@ -0,0 +1,16 @@
#macro("content"):
<div class="container has-text-centered">
<div class="image is-96x96 is-inline-block has-text-danger m-0">#icon("weui/error-outlined")</div>
<p class="is-size-4 has-text-danger">#(err)</p>
<div class="is-inline-block">
<a href="javascript:window.history.back()">
<div class="icon-text has-text-centered">
<span class="icon is-aligned">#icon("ri/arrow-left-line")</span>
<span>Go Back</span>
</div>
</a>
</div>
</div>
#!macro
#include("base.html", page = "Error")

163
templates/home.html Normal file
View File

@@ -0,0 +1,163 @@
#macro("head"):
<script>
async function getSuggestions(repo, input) {
input = input.trim();
if (repo.length == 0 || input.length == 0) return [];
const res = await fetch("/suggestions?" + new URLSearchParams({
'input': input,
'repo': repo,
}))
if (!res.ok) {
let resData = await res.json();
throw new Error(`[suggestions] ${resData.error} (HTTP ${res.status})`)
}
return await res.json();
}
function pushTag(tags, input) {
const splitTag = input.value.split('=');
if (splitTag.length != 2) {
input.classList.add('is-danger');
} else {
input.classList.remove('is-danger');
tags.push(splitTag);
input.value = "";
}
}
</script>
#!macro
#macro("content"):
<div class="is-flex is-flex-direction-column is-align-items-center image is-16x9 mb-4">
<img src="/assets/logo/distrohop.svg" style="max-width: 500px" alt="Distrohop Logo">
</div>
<section
x-data="{activeTab: new URLSearchParams(location.search).get('tab') || 'pkg', nav: false}"
x-init="$watch('activeTab', (val) => {
if (nav) {
nav = false;
return;
}
const url = new URL(window.location.href);
url.searchParams.set('tab', val);
history.pushState(null, document.title, url.toString());
})"
@popstate.window="nav = true; activeTab = new URLSearchParams(location.search).get('tab') || 'pkg'"
>
<div class="tabs is-centered">
<ul>
<li :class="{'is-active': activeTab == 'pkg'}" @click="activeTab = 'pkg'"><a>Search by Package</a></li>
<li :class="{'is-active': activeTab == 'tags'}" @click="activeTab = 'tags'"><a>Search by Tags</a></li>
</ul>
</div>
<div x-cloak x-transition:enter x-show="activeTab == 'pkg'" class="columns">
<form x-data="{'suggestions': []}" class="column is-half is-offset-one-quarter has-text-centered" action="/search/pkg">
<label class="label" for="from">Search For:</label>
<div class="field has-addons is-align-self-stretch" id="from">
<div class="control">
<span class="select">
<select name="from" x-ref="from" class="is-clipped" autocomplete="off" required>
<option selected disabled value="">Select Repo...</option>
#for(repo in cfg.Repos):
<option>#(repo.Name)</option>
#!for
</select>
</span>
</div>
<div class="control is-expanded">
<p @click.outside="suggestions = []">
<input @keyup.debounce="suggestions = await getSuggestions($refs.from.value, $refs.pkg.value)" x-ref="pkg" class="input" name="pkg" type="text" placeholder="Package Name" autocomplete="off">
</p>
<div class="dropdown is-active" x-show="suggestions.length > 0" x-anchor.bottom-start="$refs.pkg" style="z-index: 1000; width: 100%">
<div class="dropdown-content" style="width: 100%">
<template x-for="suggestion in suggestions">
<button @click.prevent="$refs.pkg.value = suggestion; suggestions = []" :title="suggestion" x-text="suggestion" class="dropdown-item is-clipped" style="text-overflow: ellipsis"></button>
</template>
</div>
</div>
</div>
</div>
<label class="label" for="in">In:</label>
<div class="field is-align-self-stretch" id="in">
<p class="control">
<span class="select is-fullwidth">
<select name="in" autocomplete="off" required>
<option selected disabled value="">Select Repo...</option>
#for(repo in cfg.Repos):
<option>#(repo.Name)</option>
#!for
</select>
</span>
</p>
</div>
<div class="field mt-4 is-align-self-stretch">
<p class="control">
<button class="button is-dark is-rounded is-fullwidth" type="submit">
<div class="icon-text">
<span class="icon is-aligned m-0">#icon("map/search")</span>
<span>Search</span>
</div>
</button>
</p>
</div>
</form>
</div>
<div x-cloak x-data="{tags: []}" x-transition:enter x-show="activeTab == 'tags'" class="columns">
<div class="column is-half is-offset-one-quarter has-text-centered">
<form action="/search/tags" x-ref="tagsForm">
<div class="field is-grouped is-grouped-multiline">
<template x-for="(tag, idx) in tags">
<div>
<div class="tags has-addons">
<span class="tag is-dark has-background-info-dark has-text-info-light" x-text="tag[0]"></span><span class="tag is-dark" x-text="tag[1]"></span><a class="tag is-delete m-0" @click.prevent="tags.splice(idx, 1)"></a>
</div>
<input class="is-hidden" name="tag" :value="tag.join('=')">
</div>
</template>
<span></span>
</div>
<div class="mt-5 field has-addons">
<div class="control is-expanded">
<input @keydown.comma.prevent="pushTag(tags, $refs.newTagInput)" class="input" x-ref="newTagInput" placeholder="bin=nano">
</div>
<div class="control">
<button class="button" @click.prevent="pushTag(tags, $refs.newTagInput)">
<div class="icon-text">
<span class="icon is-aligned m-0">#icon("icons8/plus")</span>
<span>Add</span>
</div>
</button>
</div>
</div>
<div class="field" id="in">
<p class="control">
<span class="select is-fullwidth">
<select name="in" autocomplete="off" required>
<option selected disabled value="">Select Repo...</option>
#for(repo in cfg.Repos):
<option>#(repo.Name)</option>
#!for
</select>
</span>
</p>
</div>
<button class="button is-dark is-rounded is-fullwidth" type="submit">
<div class="icon-text">
<span class="icon is-aligned m-0">#icon("map/search")</span>
<span>Search</span>
</div>
</button>
</form>
</div>
</div>
</section>
#!macro
#include("base.html", page = "Search")

21
templates/package.html Normal file
View File

@@ -0,0 +1,21 @@
#macro("content"):
<a href="javascript:window.history.back()" class="is-block">
<div class="icon-text has-text-centered">
<span class="icon is-aligned">#icon("ri/arrow-left-line")</span>
<span>Back</span>
</div>
</a>
<p class="title">#(pkg.Name)</p>
<p class="subtitle">#(inRepo)</p>
<ul>
#for(tag in pkg.Tags):
#(st = split(tag, "="))
<div class="tags has-addons my-1 mx-1">
<span class="tag mb-2 is-dark has-background-info-dark has-text-info-light">#(st[0])</span><span class="tag mb-2 is-dark">#(st[1])</span>
</div>
#!for
</ul>
#!macro
#include("base.html", page = "Package " + pkg.Name)

73
templates/results.html Normal file
View File

@@ -0,0 +1,73 @@
#macro("content"):
<p class="title mb-0">Results</p>
#if(fromRepo == ""):
<div x-data="{active: false}">
<p class="subtitle mb-2">Searching for <a @click="active = true">tags</a> in <code>#(inRepo)</code></p>
<div x-show="active" x-transition class="modal is-active">
<div class="modal-background"></div>
<div class="modal-card" @click.outside="active = false">
<header class="modal-card-head">
<p class="modal-card-title">Search Tags</p>
<button class="delete" aria-label="close" @click="active = false"></button>
</header>
<div class="modal-card-body">
<div class="field is-grouped is-grouped-multiline">
#for(tag in tags):
#(st = split(tag, "="))
<div class="tags has-addons">
<span class="tag is-dark has-background-info-dark has-text-info-light">#(st[0])</span><span class="tag is-dark">#(st[1])</span>
</div>
#!for
<span></span>
</div>
</div>
</div>
</div>
</div>
#else:
<p class="subtitle mb-2">Searching for <code>#(pkgName)</code> from <code>#(fromRepo)</code> in <code>#(inRepo)</code></p>
#!if
<p class="is-size-7 has-text-grey">Found #(len(results)) packages in #(procTime)</p>
<hr>
#for(result in results):
<div class="card">
<header class="card-header">
<div class="card-header-title">
<p>#(result.Package.Name)&nbsp;</p>
<p class="has-text-primary" title="Confidence Score">(#(sprintf("%.2f", result.Confidence * 100))%)</p>
</div>
<a class="card-header-icon" href="/pkg/#(inRepo)/#(result.Package.Name)" title="See all tags">
<span class="icon">#icon("gridicons/external")</span>
</a>
</header>
<div class="card-content">
<div x-data="{'active': false}" class="pkg-tags" x-ref="tags" :class="active && 'is-active'">
#for(tag in result.Overlap):
#(st = split(tag, "="))
<div class="tags has-addons is-display-inline-block my-1 mx-1">
<span class="tag is-dark has-background-info-dark has-text-info-light">#(st[0])</span><span class="tag is-dark">#(st[1])</span>
</div>
#!for
<template x-if="$refs.tags.childElementCount > 11">
<button class="tag is-inline-block is-dark has-background-primary-dark has-text-primary-light" @click="active = !active">
<div class="icon-text">
<template x-if="active">
<span class="icon is-aligned">#icon("ri/arrow-left-line")</span>
</template>
<span x-text="active ? 'Show Less' : 'Show More'"></span>
<template x-if="!active">
<span class="icon is-aligned">#icon("ri/arrow-right-line")</span>
</template>
</div>
</button>
</template>
</div>
</div>
</div>
#!for
#if(len(results) == 0):
<p class="has-text-centered has-text-danger subtitle">No results found :(</p>
#!if
#!macro
#include("base.html", page = "Results")