Compare commits
9
Commits
c2aae2afc3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26c6e7771b | ||
|
|
7cca8cd232 | ||
|
|
128f34c997 | ||
|
|
66eed73c0e | ||
|
|
5707282513 | ||
|
|
e453fa5559 | ||
|
|
9bd19c8ac2 | ||
|
|
330ad09048 | ||
|
|
3b7cb32b57 |
+44
-3
@@ -1,14 +1,55 @@
|
|||||||
FROM recoll
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
|
# install important dependencies
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
antiword \
|
||||||
|
aspell \
|
||||||
|
aspell-en \
|
||||||
|
git \
|
||||||
|
libxml2 \
|
||||||
|
libimage-exiftool-perl \
|
||||||
|
net-tools \
|
||||||
|
poppler-utils \
|
||||||
|
python3 \
|
||||||
|
python3-chm \
|
||||||
|
python3-libxml2 \
|
||||||
|
python3-lxml \
|
||||||
|
python3-mutagen \
|
||||||
|
python3-pip \
|
||||||
|
unrtf \
|
||||||
|
unzip \
|
||||||
|
vim
|
||||||
|
|
||||||
|
# install recollcmd from recolls programmers website
|
||||||
|
COPY recoll.gpg /etc/apt/trusted.gpg.d/recoll.gpg
|
||||||
|
RUN apt-get install --reinstall -y ca-certificates
|
||||||
|
RUN apt-get update
|
||||||
|
RUN echo deb http://www.lesbonscomptes.com/recoll/debian/ buster main > \
|
||||||
|
/etc/apt/sources.list.d/recoll.list
|
||||||
|
RUN echo deb-src http://www.lesbonscomptes.com/recoll/debian/ buster main >> \
|
||||||
|
/etc/apt/sources.list.d/recoll.list
|
||||||
|
RUN apt-get install -y --no-install-recommends recollcmd python3-recoll
|
||||||
|
RUN apt-get autoremove
|
||||||
|
|
||||||
RUN apt-get -y install breeze-icon-theme python3-venv
|
RUN apt-get -y install breeze-icon-theme python3-venv
|
||||||
|
|
||||||
ARG SQUIRREL_VERSION=0.3.1
|
# clean up
|
||||||
|
RUN apt-get clean
|
||||||
|
|
||||||
|
ARG SQUIRREL_VERSION=0.4.1
|
||||||
|
|
||||||
COPY dist/squirrel-${SQUIRREL_VERSION}-py3-none-any.whl squirrel-${SQUIRREL_VERSION}-py3-none-any.whl
|
COPY dist/squirrel-${SQUIRREL_VERSION}-py3-none-any.whl squirrel-${SQUIRREL_VERSION}-py3-none-any.whl
|
||||||
|
|
||||||
RUN python3 -m venv --system-site-packages /opt/squirrel
|
RUN python3 -m venv --system-site-packages /opt/squirrel
|
||||||
RUN /opt/squirrel/bin/pip install squirrel-${SQUIRREL_VERSION}-py3-none-any.whl
|
RUN /opt/squirrel/bin/pip install squirrel-${SQUIRREL_VERSION}-py3-none-any.whl
|
||||||
|
RUN /opt/squirrel/bin/pip install gunicorn
|
||||||
|
COPY squirrel-cmd /opt/squirrel/bin/squirrel
|
||||||
|
|
||||||
|
ENV PATH /opt/squirrel/bin:${PATH}
|
||||||
|
|
||||||
|
ENV SQUIRREL_CONFIG /etc/squirrel/config.json
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
ENTRYPOINT ["/opt/squirrel/bin/squirrel"]
|
CMD ["/opt/squirrel/bin/squirrel"]
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Squirrel
|
||||||
|
|
||||||
|
**Find your nuts**
|
||||||
|
|
||||||
|
Squirrel is a web interface to the excellent Recoll file search engine. It provides a modern
|
||||||
|
search engine interface for a headless first experience.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry install
|
||||||
|
poetry build
|
||||||
|
pip install dist/squirrel-0.3.x-py3-none-any.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -v /srv/archive:/mnt/docs:ro -p 8000:8000 squirrel
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the package and image
|
||||||
|
poetry build
|
||||||
|
docker build . -t squirrel
|
||||||
|
|
||||||
|
# Mount the source and start in watching mode
|
||||||
|
docker run -v ./data:/var/recoll -v /srv/archive:/mnt/docs:ro -v .:/app:ro -e 8000:8000 --entrypoint bash squirrel
|
||||||
|
$ /opt/squirrel/bin/python3 -m squirrel.server
|
||||||
|
```
|
||||||
|
|||||||
+6
-4
@@ -1,18 +1,20 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "squirrel"
|
name = "squirrel"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
description = "Find your nuts"
|
description = "Find your nuts"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Your Name",email = "you@example.com"}
|
{name = "trisf"}
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard] (>=0.118.0,<0.119.0)"
|
"flask (>=3.1.2,<4.0.0)",
|
||||||
|
"pillow (>=11.3.0,<12.0.0)",
|
||||||
|
"dotenv (>=0.9.9,<0.10.0)"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
"squirrel" = "squirrel.server:daemon"
|
"sqindex" = "squirrel.cmd:reindex"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
|||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
=ZCmr
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
/opt/squirrel/bin/gunicorn -w 4 -b 0.0.0.0 "squirrel.main:app"
|
||||||
+39
-89
@@ -1,107 +1,57 @@
|
|||||||
from math import ceil
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||||
from fastapi import FastAPI, Request, BackgroundTasks
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
from typing import Annotated, Any
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from typing import Any
|
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
from squirrel.auth import User, auth_user, auth_user_or_guest # type: ignore
|
||||||
import os.path
|
from squirrel.conf import settings # type: ignore
|
||||||
from dotenv import dotenv_values
|
from squirrel import repo # type:ignore
|
||||||
|
|
||||||
from recoll import recoll
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
config = {**dotenv_values(".env"), **os.environ}
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
SQUIRREL_APP = os.path.dirname(os.path.realpath(__file__)) + "/app"
|
archive = repo.get_archive()
|
||||||
|
|
||||||
SQUIRREL_ICONS = config.get("SQUIRREL_ICONS", "/usr/share/icons/breeze/mimetypes/32")
|
|
||||||
|
|
||||||
DOC_FOLDER = config.get("SQUIRREL_DOCS", "/mnt/docs")
|
|
||||||
|
|
||||||
STRIP_CHARS = len(DOC_FOLDER) + len("file:///")
|
|
||||||
|
|
||||||
|
|
||||||
class Recoll(object):
|
@router.get("/reindex")
|
||||||
def __init__(self):
|
async def get_reindex(
|
||||||
self._indexing = False
|
user: Annotated[User, Depends(auth_user)],
|
||||||
|
tasks: BackgroundTasks,
|
||||||
def reindex(self):
|
repo: str = "main",
|
||||||
if self._indexing:
|
) -> dict[str, str]:
|
||||||
raise RuntimeError("Reindex already underway")
|
if repo not in user.repos:
|
||||||
self._indexing = True
|
raise HTTPException(status_code=403, detail="No privelleges on that repo")
|
||||||
subprocess.call(["recollindex"])
|
tasks.add_task(archive.reindex, repo)
|
||||||
self._indexing = False
|
return {"message": f"Started indexing of {repo}"}
|
||||||
|
|
||||||
|
|
||||||
repo = Recoll()
|
@router.get("/search")
|
||||||
|
def get_search(
|
||||||
app = FastAPI()
|
user: Annotated[User, Depends(auth_user_or_guest)],
|
||||||
app.mount("/static", StaticFiles(directory=f"{SQUIRREL_APP}/static"))
|
q: str,
|
||||||
app.mount("/docs", StaticFiles(directory=DOC_FOLDER))
|
count: int = 20,
|
||||||
app.mount(
|
offset: int = 1,
|
||||||
"/icons",
|
) -> dict[str, Any]:
|
||||||
StaticFiles(directory=SQUIRREL_ICONS, follow_symlink=True),
|
return archive.query(q, count, offset, extra=user.repos)
|
||||||
)
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory=f"{SQUIRREL_APP}/templates")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@router.get("/repos")
|
||||||
def home(request: Request):
|
def get_repos():
|
||||||
ctx = {"request": request}
|
return archive.user_repos("foo")
|
||||||
return templates.TemplateResponse("home.html", context=ctx)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/search")
|
@router.get("/complete")
|
||||||
def search_results(request: Request, q: str, count: int = 20, page: int = 1):
|
def get_completions(q: str, field: str = "") -> list[str]:
|
||||||
ctx: dict[str, Any] = {"request": request}
|
if ":" in q:
|
||||||
ctx["search"] = search(q, count, page)
|
field, _, q = q.partition(":")
|
||||||
return templates.TemplateResponse("search_results.html", context=ctx)
|
|
||||||
|
|
||||||
|
db = archive.db([])
|
||||||
@app.get("/api/reindex")
|
result = db.termMatch("wildcard", q + "*", field=field, maxlen=10)
|
||||||
async def get_reindex(tasks: BackgroundTasks) -> dict[str, str]:
|
if field:
|
||||||
tasks.add_task(repo.reindex)
|
result = [f"{field}:{x}" for x in result]
|
||||||
return {"message": "Recoll index started"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/search")
|
|
||||||
def get_search(q: str, count: int = 20, page: int = 1) -> dict[str, Any]:
|
|
||||||
return search(q, count, page)
|
|
||||||
|
|
||||||
|
|
||||||
def search(q: str, count: int, page: int):
|
|
||||||
db = recoll.connect()
|
|
||||||
query = db.query()
|
|
||||||
nres = query.execute(q)
|
|
||||||
result = {
|
|
||||||
"query": q,
|
|
||||||
"count": count,
|
|
||||||
"page": page,
|
|
||||||
"pages": ceil(nres / count),
|
|
||||||
"total": nres,
|
|
||||||
"xquery": query.getxquery(),
|
|
||||||
"results": [],
|
|
||||||
}
|
|
||||||
if page > result["pages"] or page < 1:
|
|
||||||
return result
|
|
||||||
|
|
||||||
query.scroll((page - 1) * count)
|
|
||||||
docs = query.fetchmany(count)
|
|
||||||
for doc in docs:
|
|
||||||
d = dict(doc)
|
|
||||||
d["snippets"] = query.getsnippets(doc, ctxwords=20)
|
|
||||||
d["fpath"] = d["url"][STRIP_CHARS:]
|
|
||||||
d["title"] = d["title"] or d["filename"]
|
|
||||||
result["results"].append(d)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/info")
|
@router.get("/settings")
|
||||||
def get_info():
|
def get_info():
|
||||||
db = recoll.connect()
|
return settings
|
||||||
return dir(db)
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
SPAN.rclmatch {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-red-900);
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: beyondTheMountains; /* set name */
|
|
||||||
src: url(/static/fonts/beyond_the_mountains.ttf); /* url of the font */
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-beyond-the-mountains {
|
|
||||||
font-family: beyondTheMountains;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<html>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Squirrel</title>
|
|
||||||
<link rel="icon" type="image/png" href="/static/images/squirrel_32.png">
|
|
||||||
<link rel="stylesheet" href="/static/css/search.css"></link>
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
{% block scripts %}{% endblock %}
|
|
||||||
<head>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% block content %}CONTENT{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div x-data="query">
|
|
||||||
<div id="search-header" class="flex m-2 items-center border-b pb-2 border-red-900 mx-10">
|
|
||||||
<a href="/" class="mx-10 text-2xl text-red-900 font-beyond-the-mountains">Squirrel</a>
|
|
||||||
<form action="/search" method="get">
|
|
||||||
<input
|
|
||||||
class="shadow appearance-none border-slate-600 rounded-full w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
||||||
type="text" size="80" name="q"
|
|
||||||
onfocus="this.select()"
|
|
||||||
x-bind:value="query"></input>
|
|
||||||
</form>
|
|
||||||
<div class="grow text-right">
|
|
||||||
Foo
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="search-main" class="mx-auto w-2/3">
|
|
||||||
<div id="search-info" class="flex">
|
|
||||||
<p class="grow"></p>
|
|
||||||
<p>Page <span x-text="page"></span> of <span x-text="pages"></span>,
|
|
||||||
<span x-text="total"></span> results.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="search-results">
|
|
||||||
<template x-for="result in results">
|
|
||||||
<div class="p-4 m-4">
|
|
||||||
<div class="m-2 flex">
|
|
||||||
<img class="w-16 h-16 m-2" x-bind:src="'/icons/' + result.mtype.replace('/', '-') + '.svg'"></img>
|
|
||||||
<div>
|
|
||||||
<a class="text-xl text-red-900" x-text="result.title" x-bind:href="'/docs/' + result.fpath" target="_blank"></a><br/>
|
|
||||||
<span class="text-sm" x-text="result.relevancyrating"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<template x-for="snippet in result.snippets">
|
|
||||||
<span class="text-sm"><span x-html="snippet[2]"></span><br/></span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="search-footer" class="text-sm">
|
|
||||||
XQuery: <span x-text="xquery"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<script>
|
|
||||||
var query = {{ search|tojson }};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
request,
|
||||||
|
render_template,
|
||||||
|
session,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
flash,
|
||||||
|
g,
|
||||||
|
)
|
||||||
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from squirrel.conf import settings
|
||||||
|
import functools
|
||||||
|
|
||||||
|
bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class User:
|
||||||
|
name: str
|
||||||
|
password: str
|
||||||
|
repos: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
users = settings["users"]
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/login", methods=("GET", "POST"))
|
||||||
|
def login():
|
||||||
|
username = ""
|
||||||
|
referer = request.args.get("referer") or request.headers.get("Referer")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
username = request.form["username"]
|
||||||
|
password = request.form["password"]
|
||||||
|
error = None
|
||||||
|
|
||||||
|
user = users.get(username)
|
||||||
|
|
||||||
|
if user is None or not check_password_hash(user["password"], password):
|
||||||
|
error = "Incorrect username or password."
|
||||||
|
|
||||||
|
if error is None:
|
||||||
|
session.clear()
|
||||||
|
session["user"] = username
|
||||||
|
return redirect(referer or url_for("render_home"))
|
||||||
|
|
||||||
|
flash(error)
|
||||||
|
|
||||||
|
return render_template("auth/login.html", username=username, referer=referer)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/generate", methods=("GET", "POST"))
|
||||||
|
def generate_password():
|
||||||
|
hashed = None
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
password = request.form["password"]
|
||||||
|
hashed = generate_password_hash(password)
|
||||||
|
|
||||||
|
return render_template("auth/generate.html", hashed=hashed)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/logout")
|
||||||
|
def logout():
|
||||||
|
referer = request.headers.get("Referer")
|
||||||
|
session.clear()
|
||||||
|
return redirect(referer or url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(view):
|
||||||
|
@functools.wraps(view)
|
||||||
|
def wrapped_view(**kwargs):
|
||||||
|
if g.user is None:
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
return view(**kwargs)
|
||||||
|
|
||||||
|
return wrapped_view
|
||||||
|
|
||||||
|
|
||||||
|
@bp.before_app_request
|
||||||
|
def load_logged_in_user():
|
||||||
|
username = session.get("user")
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
g.user = User(**users["guest"])
|
||||||
|
else:
|
||||||
|
g.user = User(**users[username])
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from squirrel.repo import get_archive
|
||||||
|
import sys
|
||||||
|
|
||||||
|
archive = get_archive()
|
||||||
|
|
||||||
|
|
||||||
|
def reindex():
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("repo")
|
||||||
|
options = parser.parse_args()
|
||||||
|
|
||||||
|
p = archive.reindex(options.repo)
|
||||||
|
for line in p.stderr: # type:ignore
|
||||||
|
sys.stderr.write(line.decode("utf8"))
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import os.path
|
||||||
|
import json
|
||||||
|
from dotenv import dotenv_values
|
||||||
|
|
||||||
|
env = {**dotenv_values(".env"), **os.environ}
|
||||||
|
|
||||||
|
SQUIRREL_DIR = os.path.expanduser(env.get("SQUIRREL_DIR", "~/.config/squirrel"))
|
||||||
|
SQUIRREL_CONFIG = env.get("SQUIRREL_CONFIG", os.path.join(SQUIRREL_DIR, "config.json"))
|
||||||
|
|
||||||
|
DEFAULT_SETTINGS = {}
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
"base_dir": SQUIRREL_DIR,
|
||||||
|
"config_file": SQUIRREL_CONFIG,
|
||||||
|
"app_dir": os.path.dirname(os.path.realpath(__file__)) + "/app",
|
||||||
|
"icon_dir": env.get("SQUIREL_ICONS", "/usr/share/icons/breeze/mimetypes/32"),
|
||||||
|
}
|
||||||
|
settings.update(DEFAULT_SETTINGS)
|
||||||
|
|
||||||
|
with open(SQUIRREL_CONFIG, "r") as f:
|
||||||
|
settings.update(json.load(f))
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template,
|
||||||
|
redirect,
|
||||||
|
request,
|
||||||
|
g,
|
||||||
|
send_file,
|
||||||
|
jsonify,
|
||||||
|
Response,
|
||||||
|
)
|
||||||
|
from flask.logging import default_handler
|
||||||
|
from werkzeug.exceptions import BadRequest, NotFound, Forbidden
|
||||||
|
import io
|
||||||
|
|
||||||
|
from squirrel.conf import settings
|
||||||
|
from squirrel import auth, repo
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
# Enable logging on repo
|
||||||
|
repo.logger.addHandler(default_handler)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.config["SECRET_KEY"] = "foobar23"
|
||||||
|
|
||||||
|
# TODO: better singleton methods
|
||||||
|
archive = repo.get_archive()
|
||||||
|
|
||||||
|
app.register_blueprint(auth.bp)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def render_home():
|
||||||
|
return render_template("home.html")
|
||||||
|
|
||||||
|
|
||||||
|
def search_params(d):
|
||||||
|
"""
|
||||||
|
Reference:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
"q": d.get("q"),
|
||||||
|
"count": int(d.get("count", 20)),
|
||||||
|
"offset": int(d.get("offset", 0)),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise BadRequest(str(e))
|
||||||
|
if not params["q"]:
|
||||||
|
raise BadRequest("Missing q parameter")
|
||||||
|
if params["offset"] < 0:
|
||||||
|
raise BadRequest("Invalid offset")
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def user_repos(user):
|
||||||
|
if not user:
|
||||||
|
return []
|
||||||
|
return user.repos or []
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/search/<doctype>")
|
||||||
|
@app.get("/search")
|
||||||
|
def render_search(doctype="general"):
|
||||||
|
try:
|
||||||
|
search = archive.query(**search_params(request.args), extra=user_repos(g.user))
|
||||||
|
except BadRequest:
|
||||||
|
return redirect("/")
|
||||||
|
return render_template(
|
||||||
|
"search_results.html",
|
||||||
|
**search,
|
||||||
|
doctype=doctype,
|
||||||
|
renderer=f"results_{doctype}.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/search-fragment/<doctype>")
|
||||||
|
def render_search_fragment(doctype):
|
||||||
|
search = archive.query(**search_params(request.args), extra=user_repos(g.user))
|
||||||
|
return render_template(f"results_{doctype}.html", **search)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/icon/<type>/<subtype>.svg")
|
||||||
|
def get_icon(type, subtype):
|
||||||
|
return send_file(os.path.join(settings["icon_dir"], f"{type}-{subtype}.svg"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/doc/<repo>/<path:udi>")
|
||||||
|
def get_document(repo, udi):
|
||||||
|
if repo != "main" and repo not in user_repos(g.user):
|
||||||
|
raise Forbidden("No access to repo")
|
||||||
|
|
||||||
|
filepath = archive.path_for_udi(repo, "/" + udi)
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
raise NotFound("No such document")
|
||||||
|
return send_file(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/preview/<repo>/<path:docpath>")
|
||||||
|
def get_preview(repo, docpath):
|
||||||
|
if repo not in user_repos(g.user):
|
||||||
|
raise Forbidden("No access to repo")
|
||||||
|
|
||||||
|
try:
|
||||||
|
im = Image.open("/" + docpath)
|
||||||
|
new_size = (200, 200)
|
||||||
|
resized_img = im.resize(new_size)
|
||||||
|
# Save the image to a buffer
|
||||||
|
img_io = io.BytesIO()
|
||||||
|
resized_img.save(img_io, "JPEG", quality=70)
|
||||||
|
img_io.seek(0)
|
||||||
|
return send_file(img_io, mimetype="image/jpeg")
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/info")
|
||||||
|
def get_info():
|
||||||
|
return jsonify(archive.config)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/reindex/<repo>")
|
||||||
|
def get_reindex(repo):
|
||||||
|
p = archive.reindex(repo)
|
||||||
|
return Response(p.stderr, mimetype="text/plain")
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
Original WebUI: https://framagit.org/medoc92/recollwebui
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import configparser
|
||||||
|
import subprocess
|
||||||
|
from recoll import recoll # type:ignore
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from functools import cache
|
||||||
|
from squirrel.conf import settings # type:ignore
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
STRIP_CHARS = len("file://")
|
||||||
|
|
||||||
|
logger.info("TEST")
|
||||||
|
|
||||||
|
|
||||||
|
class Squirrel(object):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.repos = self.config["repos"]
|
||||||
|
self.extra_dbs = {}
|
||||||
|
self.paths = {}
|
||||||
|
|
||||||
|
for name, config_path in self.repos.items():
|
||||||
|
logger.info("Loading repo %s [%s]", name, config_path)
|
||||||
|
# Really basic RCLConfig parser
|
||||||
|
parser = configparser.ConfigParser(allow_unnamed_section=True)
|
||||||
|
if not parser.read(os.path.join(config_path, "recoll.conf")):
|
||||||
|
raise IOError(f"Failed to open config at {config_path}")
|
||||||
|
repo: dict[str, str] = dict(parser.items(configparser.UNNAMED_SECTION)) # type:ignore
|
||||||
|
|
||||||
|
for d in repo.get("topdirs", "").split():
|
||||||
|
self.paths[d] = name
|
||||||
|
|
||||||
|
self.extra_dbs[name] = os.path.join(
|
||||||
|
repo.get("cachedir", config_path), "xapiandb"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.search_paths = list(self.paths.keys())
|
||||||
|
self.search_paths.sort(reverse=True)
|
||||||
|
logger.info("Search paths: %s", self.search_paths)
|
||||||
|
|
||||||
|
def db(self, repo: str = "main", extra: list[str] = []):
|
||||||
|
dbs = [db for name, db in self.extra_dbs.items() if name in extra]
|
||||||
|
|
||||||
|
return recoll.connect(self.repos[repo], extra_dbs=dbs)
|
||||||
|
|
||||||
|
def reindex(self, repo="main"):
|
||||||
|
config = self.config["repos"][repo]
|
||||||
|
p = subprocess.Popen(["recollindex", "-c", config], stderr=subprocess.PIPE)
|
||||||
|
return p
|
||||||
|
|
||||||
|
def get_repos(self, filename):
|
||||||
|
return {self.paths[x] for x in self.search_paths if filename.startswith(x)}
|
||||||
|
|
||||||
|
def query(
|
||||||
|
self,
|
||||||
|
q: str,
|
||||||
|
count: int = 20,
|
||||||
|
offset: int = 0,
|
||||||
|
repo: str = "main",
|
||||||
|
extra: list[str] = [],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
db = self.db(repo, extra)
|
||||||
|
query = db.query()
|
||||||
|
nres = query.execute(q)
|
||||||
|
s_extra = set(extra + ["main"])
|
||||||
|
result = {
|
||||||
|
"query": q,
|
||||||
|
"count": count,
|
||||||
|
"start": offset,
|
||||||
|
"end": offset,
|
||||||
|
"total": nres,
|
||||||
|
"xquery": query.getxquery(),
|
||||||
|
"repos": [repo] + extra,
|
||||||
|
"results": [],
|
||||||
|
}
|
||||||
|
if offset >= nres or offset < 0:
|
||||||
|
return result
|
||||||
|
if nres == 0:
|
||||||
|
return result
|
||||||
|
|
||||||
|
if offset > 0:
|
||||||
|
query.scroll(offset)
|
||||||
|
docs = query.fetchmany(count)
|
||||||
|
for doc in docs:
|
||||||
|
d = dict(doc)
|
||||||
|
d["udi"] = doc.rcludi
|
||||||
|
d["snippets"] = query.getsnippets(doc, ctxwords=10)
|
||||||
|
d["fpath"] = d["url"][STRIP_CHARS:]
|
||||||
|
d["title"] = d["title"] or d["filename"]
|
||||||
|
d["repos"] = list(self.get_repos(d["fpath"]) & s_extra)
|
||||||
|
result["results"].append(d)
|
||||||
|
result["end"] += 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
def path_for_udi(self, repo, udi: str):
|
||||||
|
db = recoll.connect(self.config["repos"][repo])
|
||||||
|
doc = db.getDoc(udi)
|
||||||
|
if not doc or not doc.url:
|
||||||
|
raise IOError("No such file")
|
||||||
|
return doc.url[STRIP_CHARS:]
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get_archive():
|
||||||
|
return Squirrel(settings)
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import uvicorn
|
|
||||||
|
|
||||||
|
|
||||||
def daemon(reload=False):
|
|
||||||
uvicorn.run("squirrel.api:app", host="0.0.0.0", port=8000, reload=reload)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
daemon(True)
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
.flashes {
|
||||||
|
@apply bg-amber-300 my-2 mx-20 px-10 py-2 border border-amber-600 rounded-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@apply mx-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
@apply border rounded bg-gray-100 px-2 py-1 text-red-900 font-bold hover:cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
|
||||||
|
.field {
|
||||||
|
@apply border rounded py-2 px-3 text-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
@apply mb-2 font-bold text-red-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-control {
|
||||||
|
@apply flex flex-col mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPAN.rclmatch {
|
||||||
|
@apply font-bold text-red-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom fonts */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: beyondTheMountains; /* set name */
|
||||||
|
src: url(/static/fonts/beyond_the_mountains.ttf); /* url of the font */
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-beyond-the-mountains {
|
||||||
|
font-family: beyondTheMountains;
|
||||||
|
}
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
/*! tailwindcss v4.1.14 | MIT License | https://tailwindcss.com */
|
||||||
|
@layer properties;
|
||||||
@layer theme, base, components, utilities;
|
@layer theme, base, components, utilities;
|
||||||
@layer theme {
|
@layer theme {
|
||||||
:root, :host {
|
:root, :host {
|
||||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||||
'Noto Color Emoji';
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
monospace;
|
"Courier New", monospace;
|
||||||
--color-red-900: oklch(0.396 0.141 25.723);
|
--color-red-300: oklch(80.8% 0.114 19.571);
|
||||||
--color-slate-600: oklch(0.446 0.043 257.281);
|
--color-red-600: oklch(57.7% 0.245 27.325);
|
||||||
--color-gray-700: oklch(0.373 0.034 259.733);
|
--color-red-900: oklch(39.6% 0.141 25.723);
|
||||||
|
--color-amber-300: oklch(87.9% 0.169 91.605);
|
||||||
|
--color-amber-600: oklch(66.6% 0.179 58.318);
|
||||||
|
--color-slate-300: oklch(86.9% 0.022 252.894);
|
||||||
|
--color-slate-600: oklch(44.6% 0.043 257.281);
|
||||||
|
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||||
|
--color-gray-700: oklch(37.3% 0.034 259.733);
|
||||||
|
--color-gray-900: oklch(21% 0.034 264.665);
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
|
--container-md: 28rem;
|
||||||
--text-sm: 0.875rem;
|
--text-sm: 0.875rem;
|
||||||
--text-sm--line-height: calc(1.25 / 0.875);
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
--text-xl: 1.25rem;
|
--text-xl: 1.25rem;
|
||||||
@@ -18,13 +27,11 @@
|
|||||||
--text-2xl--line-height: calc(2 / 1.5);
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
--text-8xl: 6rem;
|
--text-8xl: 6rem;
|
||||||
--text-8xl--line-height: 1;
|
--text-8xl--line-height: 1;
|
||||||
|
--font-weight-bold: 700;
|
||||||
--leading-tight: 1.25;
|
--leading-tight: 1.25;
|
||||||
|
--radius-xl: 0.75rem;
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
--default-font-feature-settings: var(--font-sans--font-feature-settings);
|
|
||||||
--default-font-variation-settings: var(--font-sans--font-variation-settings);
|
|
||||||
--default-mono-font-family: var(--font-mono);
|
--default-mono-font-family: var(--font-mono);
|
||||||
--default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
|
|
||||||
--default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -38,14 +45,11 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' );
|
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
body {
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
hr {
|
hr {
|
||||||
height: 0;
|
height: 0;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -68,7 +72,7 @@
|
|||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
code, kbd, samp, pre {
|
code, kbd, samp, pre {
|
||||||
font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace );
|
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
|
||||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@@ -134,7 +138,14 @@
|
|||||||
}
|
}
|
||||||
::placeholder {
|
::placeholder {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: color-mix(in oklab, currentColor 50%, transparent);
|
}
|
||||||
|
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
||||||
|
::placeholder {
|
||||||
|
color: currentcolor;
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
@@ -155,16 +166,19 @@
|
|||||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||||
padding-block: 0;
|
padding-block: 0;
|
||||||
}
|
}
|
||||||
|
::-webkit-calendar-picker-indicator {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
:-moz-ui-invalid {
|
:-moz-ui-invalid {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
|
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
|
||||||
appearance: button;
|
appearance: button;
|
||||||
}
|
}
|
||||||
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
[hidden]:where(:not([hidden='until-found'])) {
|
[hidden]:where(:not([hidden="until-found"])) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,18 +189,36 @@
|
|||||||
.m-4 {
|
.m-4 {
|
||||||
margin: calc(var(--spacing) * 4);
|
margin: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.m-10 {
|
||||||
|
margin: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
.mx-10 {
|
.mx-10 {
|
||||||
margin-inline: calc(var(--spacing) * 10);
|
margin-inline: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
.my-5 {
|
||||||
|
margin-block: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
.my-10 {
|
.my-10 {
|
||||||
margin-block: calc(var(--spacing) * 10);
|
margin-block: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
.mt-50 {
|
.mt-50 {
|
||||||
margin-top: calc(var(--spacing) * 50);
|
margin-top: calc(var(--spacing) * 50);
|
||||||
}
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -196,6 +228,18 @@
|
|||||||
.h-16 {
|
.h-16 {
|
||||||
height: calc(var(--spacing) * 16);
|
height: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
|
.h-18 {
|
||||||
|
height: calc(var(--spacing) * 18);
|
||||||
|
}
|
||||||
|
.h-20 {
|
||||||
|
height: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
|
.h-22 {
|
||||||
|
height: calc(var(--spacing) * 22);
|
||||||
|
}
|
||||||
|
.h-64 {
|
||||||
|
height: calc(var(--spacing) * 64);
|
||||||
|
}
|
||||||
.w-1\/2 {
|
.w-1\/2 {
|
||||||
width: calc(1/2 * 100%);
|
width: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
@@ -205,9 +249,24 @@
|
|||||||
.w-16 {
|
.w-16 {
|
||||||
width: calc(var(--spacing) * 16);
|
width: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
|
.w-18 {
|
||||||
|
width: calc(var(--spacing) * 18);
|
||||||
|
}
|
||||||
|
.w-20 {
|
||||||
|
width: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
|
.w-22 {
|
||||||
|
width: calc(var(--spacing) * 22);
|
||||||
|
}
|
||||||
|
.w-64 {
|
||||||
|
width: calc(var(--spacing) * 64);
|
||||||
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.max-w-md {
|
||||||
|
max-width: var(--container-md);
|
||||||
|
}
|
||||||
.grow {
|
.grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
@@ -217,9 +276,24 @@
|
|||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
.rounded-full {
|
.rounded-full {
|
||||||
border-radius: calc(infinity * 1px);
|
border-radius: calc(infinity * 1px);
|
||||||
}
|
}
|
||||||
|
.rounded-l-full {
|
||||||
|
border-top-left-radius: calc(infinity * 1px);
|
||||||
|
border-bottom-left-radius: calc(infinity * 1px);
|
||||||
|
}
|
||||||
|
.rounded-r-full {
|
||||||
|
border-top-right-radius: calc(infinity * 1px);
|
||||||
|
border-bottom-right-radius: calc(infinity * 1px);
|
||||||
|
}
|
||||||
|
.border {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
.border-b {
|
.border-b {
|
||||||
border-bottom-style: var(--tw-border-style);
|
border-bottom-style: var(--tw-border-style);
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
@@ -230,6 +304,18 @@
|
|||||||
.border-slate-600 {
|
.border-slate-600 {
|
||||||
border-color: var(--color-slate-600);
|
border-color: var(--color-slate-600);
|
||||||
}
|
}
|
||||||
|
.bg-red-300 {
|
||||||
|
background-color: var(--color-red-300);
|
||||||
|
}
|
||||||
|
.bg-red-600 {
|
||||||
|
background-color: var(--color-red-600);
|
||||||
|
}
|
||||||
|
.bg-slate-300 {
|
||||||
|
background-color: var(--color-slate-300);
|
||||||
|
}
|
||||||
|
.mask-clip-border {
|
||||||
|
mask-clip: border-box;
|
||||||
|
}
|
||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -239,6 +325,12 @@
|
|||||||
.px-5 {
|
.px-5 {
|
||||||
padding-inline: calc(var(--spacing) * 5);
|
padding-inline: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.px-10 {
|
||||||
|
padding-inline: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
|
.px-20 {
|
||||||
|
padding-inline: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
.py-2 {
|
.py-2 {
|
||||||
padding-block: calc(var(--spacing) * 2);
|
padding-block: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -251,9 +343,15 @@
|
|||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.text-justify {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
.text-right {
|
.text-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
.font-mono {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
.text-2xl {
|
.text-2xl {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
@@ -274,12 +372,18 @@
|
|||||||
--tw-leading: var(--leading-tight);
|
--tw-leading: var(--leading-tight);
|
||||||
line-height: var(--leading-tight);
|
line-height: var(--leading-tight);
|
||||||
}
|
}
|
||||||
|
.whitespace-pre {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
.text-gray-700 {
|
.text-gray-700 {
|
||||||
color: var(--color-gray-700);
|
color: var(--color-gray-700);
|
||||||
}
|
}
|
||||||
.text-red-900 {
|
.text-red-900 {
|
||||||
color: var(--color-red-900);
|
color: var(--color-red-900);
|
||||||
}
|
}
|
||||||
|
.text-slate-300 {
|
||||||
|
color: var(--color-slate-300);
|
||||||
|
}
|
||||||
.shadow {
|
.shadow {
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
@@ -291,8 +395,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.flashes {
|
||||||
|
margin-inline: calc(var(--spacing) * 20);
|
||||||
|
margin-block: calc(var(--spacing) * 2);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--color-amber-600);
|
||||||
|
background-color: var(--color-amber-300);
|
||||||
|
padding-inline: calc(var(--spacing) * 10);
|
||||||
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-inline: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
background-color: var(--color-gray-100);
|
||||||
|
padding-inline: calc(var(--spacing) * 2);
|
||||||
|
padding-block: calc(var(--spacing) * 1);
|
||||||
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-red-900);
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-red-900);
|
||||||
|
}
|
||||||
|
.field-control {
|
||||||
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
SPAN.rclmatch {
|
SPAN.rclmatch {
|
||||||
font-weight: bold;
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--color-red-900);
|
color: var(--color-red-900);
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -320,6 +474,11 @@ SPAN.rclmatch {
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@property --tw-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
@property --tw-inset-shadow {
|
@property --tw-inset-shadow {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -329,6 +488,11 @@ SPAN.rclmatch {
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@property --tw-inset-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
@property --tw-ring-color {
|
@property --tw-ring-color {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -366,3 +530,30 @@ SPAN.rclmatch {
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0 0 #0000;
|
initial-value: 0 0 #0000;
|
||||||
}
|
}
|
||||||
|
@property --tw-font-weight {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@layer properties {
|
||||||
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||||
|
*, ::before, ::after, ::backdrop {
|
||||||
|
--tw-border-style: solid;
|
||||||
|
--tw-leading: initial;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-color: initial;
|
||||||
|
--tw-shadow-alpha: 100%;
|
||||||
|
--tw-inset-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-shadow-color: initial;
|
||||||
|
--tw-inset-shadow-alpha: 100%;
|
||||||
|
--tw-ring-color: initial;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-ring-color: initial;
|
||||||
|
--tw-inset-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-inset: initial;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-font-weight: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="password-generator">
|
||||||
|
<div id="generator-form" class="max-w-md mx-auto mt-20">
|
||||||
|
<form action="/auth/generate" method="POST">
|
||||||
|
<div class="field-control">
|
||||||
|
<label class="field-label" for="password">Password</label>
|
||||||
|
<input class="field" type="password", name="password"></input>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="button" type="submit" value="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% if hashed %}
|
||||||
|
<div class="border rounded m-10 py-2 px-10">
|
||||||
|
Password: <span class="font-mono">{{ hashed }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="login page">
|
||||||
|
<div id="login-form" class="max-w-md mx-auto mt-20">
|
||||||
|
<form action="/auth/login?referer={{referer|urlencode}}" method="POST">
|
||||||
|
<div class="field-control">
|
||||||
|
<label class="field-label" for="username">User</label>
|
||||||
|
<input class="field" type="text" name="username" value="{{username}}"></input>
|
||||||
|
</div>
|
||||||
|
<div class="field-control">
|
||||||
|
<label class="field-label" for="password">Password</label>
|
||||||
|
<input class="field" type="password", name="password"></input>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="button" type="submit" value="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Squirrel</title>
|
||||||
|
<link rel="icon" type="image/png" href="/static/images/squirrel_32.png">
|
||||||
|
<link rel="stylesheet" href="/static/css/search.css"></link>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block header %}
|
||||||
|
<div id="search-header" class="flex m-2 items-center border-b pb-2 border-red-900 mx-10">
|
||||||
|
<a href="/" class="mx-10 text-2xl text-red-900 font-beyond-the-mountains">Squirrel</a>
|
||||||
|
<div id="header-main" class="header grow">
|
||||||
|
{% if query %}
|
||||||
|
<form action="/search" method="get">
|
||||||
|
<div class="shadow border-slate-600 rounded-full w-full text-gray-700 leading-tight focus:shadow-outline flex">
|
||||||
|
<div class="bg-red-300 py-2 px-3 mask-clip-border rounded-l-full">Bubble</div>
|
||||||
|
<input
|
||||||
|
class="appearance-none focus:outline-none py-2 px-3 grow"
|
||||||
|
type="text" size="80" name="q" onfocus="this.select()" value="{{ query }}"></input>
|
||||||
|
<button type="submit" class="bg-slate-300 py-2 px-3 rounded-r-full">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="header">User: {{ g.user.name }}</div>
|
||||||
|
<div class="header">
|
||||||
|
{% if g.user.name == "Guest" %}
|
||||||
|
<a href="/auth/login"><span class="material-symbols-outlined">lock_open</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/auth/logout"><span class="material-symbols-outlined">lock<span></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div>
|
||||||
|
<ul class=flashes>
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% block content %}CONTENT{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block header %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="home-header"></div>
|
<div id="home-header"></div>
|
||||||
@@ -9,6 +11,7 @@
|
|||||||
<form action="/search" method="get">
|
<form action="/search" method="get">
|
||||||
<input type="text" name="q" placeholder="Find your nuts..."
|
<input type="text" name="q" placeholder="Find your nuts..."
|
||||||
class="shadow appearance-none border-slate-600 text-2xl rounded-full w-full py-3 px-5 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
class="shadow appearance-none border-slate-600 text-2xl rounded-full w-full py-3 px-5 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||||
|
autocorrect="off" spellcheck="false" autocomplete="off"
|
||||||
></input>
|
></input>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
<div class="result-block">
|
||||||
|
{% for result in results %}
|
||||||
|
<div class="search-result mb-4">
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="m-2 flex">
|
||||||
|
<img class="w-18 h-18 m-2" src="/icon/{{result.mtype}}.svg"></img>
|
||||||
|
<div>
|
||||||
|
<a class="text-xl text-red-900" href="/doc/{{result.repos.0}}{{result.udi}}" target="_blank">{{ result.title }}</a><br/>
|
||||||
|
<span class="text-sm">{{ result.relevancyrating }}</span> <span class="text-sm">{{ result.repos|join(", ") }}</span><br>
|
||||||
|
<span class="text-sm">{{ result.fpath }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
{% for snippet in result.snippets %}
|
||||||
|
<div class="text-sm mb-2">{{ snippet[2]|safe }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr class="text-slate-300"></hr>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
<div class="result-block flex">
|
||||||
|
{% for result in results %}
|
||||||
|
<div class="search-result">
|
||||||
|
<img class="w-64 h-64 m-2" src="/icon/{{result.mtype}}.svg"></img>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div x-data="ctx()">
|
||||||
|
|
||||||
|
<div id="search-main" class="mx-auto w-2/3">
|
||||||
|
<div id="search-info" class="flex">
|
||||||
|
<p class="grow"></p>
|
||||||
|
<p>Results {{ start+1 }} to <span x-text="resultCount">{{ end+1 }}</span> of {{ total }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-results">
|
||||||
|
{% include renderer %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center my-5" x-show="resultCount < totalResults">
|
||||||
|
<button class="button px-20" x-on:click="loadMore">Load More</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-footer" class="text-sm text-right">
|
||||||
|
XQuery: <span>{{ xquery }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
console.log("ONE");
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
console.log("TWO");
|
||||||
|
Alpine.data("ctx", () => ({
|
||||||
|
resultCount: {{ end }},
|
||||||
|
totalResults: {{ total }},
|
||||||
|
loadMore(foo) {
|
||||||
|
fetch("/search-fragment/{{doctype}}?q={{query|urlencode}}&count={{count}}&offset=" + this.resultCount).then((response) => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
return response.text();
|
||||||
|
} else {
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
}).then((text) => {
|
||||||
|
let el = document.createElement("div");
|
||||||
|
el.innerHTML = text;
|
||||||
|
let results = foo = el.firstElementChild.getElementsByClassName("search-result");
|
||||||
|
this.resultCount += results.length;
|
||||||
|
document.getElementById("search-results").firstElementChild.append(...results);
|
||||||
|
})
|
||||||
|
console.log("FOO");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% if request.args.debug %}
|
||||||
|
<div class="whitespace-pre">
|
||||||
|
{{ results|tojson(1) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user