Repos / pytaku / 48becd10fe
commit 48becd10fe783ae937cfcda772f38e88659a6ffb
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Tue Aug 25 21:15:15 2020 +0700

    remove old stuff before SPA

diff --git a/src/pytaku/decorators.py b/src/pytaku/decorators.py
index 599dd36..22015e7 100644
--- a/src/pytaku/decorators.py
+++ b/src/pytaku/decorators.py
@@ -1,18 +1,8 @@
 from functools import wraps
 
-from flask import jsonify, redirect, request, session, url_for
+from flask import jsonify, request
 
-from .persistence import read, unread, verify_token
-
-
-def require_login(f):
-    @wraps(f)
-    def decorated_function(*args, **kwargs):
-        if session.get("user") is None:
-            return redirect(url_for("auth_view", next=request.url))
-        return f(*args, **kwargs)
-
-    return decorated_function
+from .persistence import verify_token
 
 
 def process_token(required=True):
@@ -48,39 +38,3 @@ def decorated_function(*args, **kwargs):
         return decorated_function
 
     return decorator
-
-
-def toggle_has_read(f):
-    """
-    Augments a view with the ability to toggle a chapter's read status if there's a
-    `?has_read=<chapter_id>` url param.
-    """
-
-    @wraps(f)
-    def decorated_function(*args, **kwargs):
-        assert "site" in kwargs
-        assert "title_id" in kwargs
-        has_read_chapter_id = request.args.get("has_read")
-        unread_chapter_id = request.args.get("unread")
-        assert not (has_read_chapter_id and unread_chapter_id)  # can't do both
-
-        if session.get("user"):
-            if has_read_chapter_id:
-                read(
-                    session["user"]["id"],
-                    kwargs["site"],
-                    kwargs["title_id"],
-                    has_read_chapter_id,
-                )
-                return redirect(request.url[: request.url.rfind("?")])
-            elif unread_chapter_id:
-                unread(
-                    session["user"]["id"],
-                    kwargs["site"],
-                    kwargs["title_id"],
-                    unread_chapter_id,
-                )
-                return redirect(request.url[: request.url.rfind("?")])
-        return f(*args, **kwargs)
-
-    return decorated_function
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 747f23e..09970cf 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -6,20 +6,10 @@
 from typing import List, Tuple
 
 import requests
-from flask import (
-    Flask,
-    flash,
-    jsonify,
-    make_response,
-    redirect,
-    render_template,
-    request,
-    session,
-    url_for,
-)
+from flask import Flask, jsonify, make_response, render_template, request, url_for
 
 from .conf import config
-from .decorators import process_token, require_login
+from .decorators import process_token
 from .persistence import (
     create_token,
     delete_token,
@@ -71,136 +61,6 @@ def _chapter_name(chapter: dict):
     return result
 
 
-@app.route("/following", methods=["GET"])
-@require_login
-def follows_view():
-    titles = get_followed_titles(session["user"]["id"])
-    for title in titles:
-        thumbnail = title_thumbnail(title["site"], title["id"])
-        if title["site"] == "mangadex":
-            thumbnail = url_for("proxy_view", b64_url=_encode_proxy_url(thumbnail))
-        title["thumbnail"] = thumbnail
-    return render_template("old/follows.html", titles=titles)
-
-
-@app.route("/follow/<site>/<title_id>", methods=["POST"])
-@require_login
-def follow_view(site, title_id):
-    follow(session["user"]["id"], site, title_id)
-    return redirect(url_for("spa_title_view", site=site, title_id=title_id))
-
-
-@app.route("/unfollow/<site>/<title_id>", methods=["POST"])
-@require_login
-def unfollow_view(site, title_id):
-    unfollow(session["user"]["id"], site, title_id)
-    return redirect(url_for("spa_title_view", site=site, title_id=title_id))
-
-
-@app.route("/logout", methods=["POST"])
-def logout_view():
-    session.pop("user")
-    return redirect("/")
-
-
-@app.route("/auth", methods=["GET", "POST"])
-def auth_view():
-    if session.get("user"):
-        return redirect(url_for("home_view"))
-
-    if request.method == "POST":
-
-        if request.form["action"] == "register":
-            username = request.form["username"].strip()
-            password = request.form["password"]
-            confirm_password = request.form["confirm-password"]
-            message = None
-            if password != confirm_password:
-                message = "Password confirmation didn't match."
-                status_code = 400
-            elif not (username and password and confirm_password):
-                message = "Empty field(s) spotted. Protip: spaces don't count."
-                status_code = 400
-            elif (
-                len(username) < 2
-                or len(username) > 15
-                or len(password) < 5
-                or len(password) > 50
-            ):
-                message = "Invalid username/password length. Username length should be 2~15, password 5~50."
-                status_code = 400
-            else:  # success!
-                err = register_user(username, password)
-                if err:
-                    message = err
-                    status_code = 400
-                else:
-                    username = ""
-                    password = ""
-                    confirm_password = ""
-                    message = "Registration successful! You can login now."
-                    status_code = 200
-            return (
-                render_template(
-                    "old/auth.html",
-                    register_username=username,
-                    register_password=password,
-                    register_confirm_password=confirm_password,
-                    register_message=message,
-                    register_has_error=status_code != 200,
-                ),
-                status_code,
-            )
-
-        else:  # action == 'login'
-            username = request.form["username"].strip()
-            password = request.form["password"]
-            remember = request.form.get("remember") == "on"
-            if not (username and password):
-                message = "Empty field(s) spotted. Protip: spaces don't count."
-                status_code = 400
-            else:
-                user_id = verify_username_password(username, password)
-                if user_id is None:
-                    message = "Wrong username/password combination."
-                    status_code = 400
-                else:  # success!
-                    resp = redirect(request.args.get("next", url_for("home_view")))
-                    session.permanent = remember
-                    session["user"] = {"username": username, "id": user_id}
-                    return resp
-
-            return (
-                render_template(
-                    "old/auth.html",
-                    login_username=username,
-                    login_password=password,
-                    login_remember=remember,
-                    login_message=message,
-                    login_has_error=status_code != 200,
-                ),
-                status_code,
-            )
-
-    # Just a plain ol' GET request:
-    return render_template("old/auth.html")
-
-
-@app.route("/search")
-def search_view():
-    query = request.args.get("q", "").strip()
-    results = {}
-    if query:
-        results = search_title_all_sites(query)
-
-    if "mangadex" in results:
-        for title in results["mangadex"]:
-            title["thumbnail"] = url_for(
-                "proxy_view", b64_url=_encode_proxy_url(title["thumbnail"])
-            )
-    return render_template("old/search.html", results=results, query=query)
-
-
 @app.route("/proxy/<b64_url>")
 def proxy_view(b64_url):
     """Fine I'll do it"""
@@ -231,41 +91,6 @@ def _is_manga_img_url(
     return pattern.match(url)
 
 
-@app.route("/import", methods=["GET", "POST"])
-@require_login
-def import_view():
-    if request.method == "POST":
-
-        # check if the post request has the file part
-        if "tachiyomi" not in request.files:
-            flash("No file part")
-            return redirect(request.url)
-        file = request.files["tachiyomi"]
-
-        # if user does not select file, browser also
-        # submits an empty part without filename
-        if file.filename == "":
-            flash("No selected file")
-            return redirect(request.url)
-
-        if file:
-            text = file.read()
-            site_title_pairs = read_tachiyomi_follows(text)
-            if site_title_pairs is None:
-                flash("Malformed input file.")
-                return redirect(request.url)
-
-            # First fetch & save titles if they're not already in db
-            ensure_titles(site_title_pairs)
-
-            # Then follow them all
-            import_follows(session["user"]["id"], site_title_pairs)
-
-            flash(f"Added {len(site_title_pairs)} follows.")
-
-    return render_template("old/import.html")
-
-
 def read_tachiyomi_follows(text: str) -> List[Tuple[str, str]]:
     try:
         data = json.loads(text)
@@ -331,11 +156,6 @@ def ensure_titles(site_title_pairs: List[Tuple[str, str]]):
     return title_dicts
 
 
-"""
-New Mithril-based SPA views follow
-"""
-
-
 @app.route("/")
 @app.route("/h")
 @app.route("/a")
diff --git a/src/pytaku/static/base.css b/src/pytaku/static/base.css
deleted file mode 100644
index babcfd1..0000000
--- a/src/pytaku/static/base.css
+++ /dev/null
@@ -1,214 +0,0 @@
-/* Look & feel */
-:root {
-  --btn-red: #ef4f39;
-  --btn-red-bottom: #b94434;
-  --btn-green: #3ba60a;
-  --btn-green-bottom: #338a0d;
-  --btn-blue: #009ee8;
-  --btn-blue-bottom: #26789f;
-  --btn-gray: #444;
-  --btn-gray-bottom: #555;
-  --bg-black: #231f20;
-  --border-radius: 3px;
-  --body-padding: 0.5rem;
-
-  font-size: 100%;
-  font-family: sans-serif;
-  overflow-y: scroll;
-}
-
-html {
-  touch-action: manipulation;
-}
-
-h1 {
-  margin-top: 1rem;
-  margin-bottom: 0.5rem;
-  font-size: 2rem;
-  font-weight: bold;
-}
-
-table {
-  border-collapse: collapse;
-}
-td,
-th {
-  border: 1px solid black;
-  padding: 0.5em;
-}
-tr:nth-child(even) {
-  background-color: #eee;
-}
-th {
-  background-color: #777;
-  color: white;
-}
-
-input {
-  padding: 0.3rem;
-}
-
-p {
-  line-height: 1.5rem;
-}
-
-.button {
-  display: inline-block;
-  user-select: none;
-  margin: 0;
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  vertical-align: bottom;
-
-  cursor: pointer;
-  border: 0;
-  padding: 0.5rem 1rem 0.3rem 1rem;
-  border-radius: var(--border-radius);
-  color: white;
-  text-decoration: none;
-}
-.button > * {
-  margin-right: 0.3rem;
-}
-.button > *:last-child {
-  margin-right: 0;
-}
-.button:hover {
-  filter: brightness(110%);
-}
-.button:active {
-  filter: brightness(90%);
-}
-.button:focus {
-  box-shadow: 0 0 2px black;
-}
-.button.red {
-  background-color: var(--btn-red);
-  border-bottom: 4px solid var(--btn-red-bottom);
-}
-.button.green {
-  background-color: var(--btn-green);
-  border-bottom: 4px solid var(--btn-green-bottom);
-}
-.button.blue {
-  background-color: var(--btn-blue);
-  border-bottom: 4px solid var(--btn-blue-bottom);
-}
-.button.gray {
-  background-color: var(--btn-gray);
-  border-bottom: 4px solid var(--btn-gray-bottom);
-}
-.button.disabled,
-.button.disabled:hover,
-.button.disabled:active {
-  background-color: #aaa;
-  border-bottom: 4px solid #aaa;
-  filter: none;
-  cursor: default;
-  opacity: 0.5;
-}
-.button > img {
-  height: 1rem;
-}
-
-/* Grid layout */
-
-nav {
-  display: flex;
-  flex-wrap: wrap;
-  padding: var(--body-padding);
-}
-nav > * {
-  margin-right: 1rem;
-  margin-top: 0.2rem;
-  margin-bottom: 0.2rem;
-}
-nav > *:last-child {
-  margin-right: 0;
-}
-
-.logo {
-  width: 150px;
-}
-.logo img {
-  max-width: 100%;
-  display: block;
-}
-
-.search {
-  display: flex;
-}
-
-.links {
-  margin-left: auto; /* to pull to the right */
-  display: inline-flex;
-  justify-content: center;
-  align-items: center;
-  color: white;
-}
-.links > * {
-  margin-right: 1rem;
-}
-.links > *:last-child {
-  margin-right: 0;
-}
-.links a * {
-  vertical-align: middle;
-}
-
-/* Component-specific styling */
-
-nav {
-  background-color: var(--bg-black);
-}
-
-.search input {
-  border: 0;
-  border-radius: var(--border-radius) 0 0 var(--border-radius);
-}
-.search button {
-  border-radius: 0 var(--border-radius) var(--border-radius) 0;
-}
-
-.links a {
-  color: white;
-  text-decoration: none;
-}
-.links a:hover {
-  text-decoration: underline;
-}
-
-.content {
-  padding: var(--body-padding);
-}
-.content > * {
-  display: block;
-  margin-top: 0.5rem;
-  margin-bottom: 0.5rem;
-}
-
-footer {
-  padding: 1rem var(--body-padding) var(--body-padding) var(--body-padding);
-  line-height: 1.5em;
-  opacity: 0.7;
-}
-footer a {
-  color: inherit;
-}
-
-/* sticky footer */
-html,
-body {
-  height: 100%;
-}
-body {
-  display: flex;
-  flex-direction: column;
-}
-.content {
-  flex: 1 0 auto;
-}
-footer {
-  flex-shrink: 0;
-}
diff --git a/src/pytaku/static/icons/LICENSE b/src/pytaku/static/icons/LICENSE
deleted file mode 100644
index 29e925b..0000000
--- a/src/pytaku/static/icons/LICENSE
+++ /dev/null
@@ -1,24 +0,0 @@
-Feather icon set by Cole Bemis:
-https://github.com/feathericons/feather
-
-The MIT License (MIT)
-
-Copyright (c) 2013-2017 Cole Bemis
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/src/pytaku/static/icons/arrow-up-right.svg b/src/pytaku/static/icons/arrow-up-right.svg
deleted file mode 100644
index b61b220..0000000
--- a/src/pytaku/static/icons/arrow-up-right.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-right"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/bookmark.svg b/src/pytaku/static/icons/bookmark.svg
deleted file mode 100644
index ce23590..0000000
--- a/src/pytaku/static/icons/bookmark.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bookmark"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/chevrons-left.svg b/src/pytaku/static/icons/chevrons-left.svg
deleted file mode 100644
index 3b96532..0000000
--- a/src/pytaku/static/icons/chevrons-left.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-left"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/chevrons-right.svg b/src/pytaku/static/icons/chevrons-right.svg
deleted file mode 100644
index 7071dfc..0000000
--- a/src/pytaku/static/icons/chevrons-right.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-right"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/eye.svg b/src/pytaku/static/icons/eye.svg
deleted file mode 100644
index 1835ad0..0000000
--- a/src/pytaku/static/icons/eye.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/list.svg b/src/pytaku/static/icons/list.svg
deleted file mode 100644
index 1c2ecba..0000000
--- a/src/pytaku/static/icons/list.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/log-in.svg b/src/pytaku/static/icons/log-in.svg
deleted file mode 100644
index 7bfc566..0000000
--- a/src/pytaku/static/icons/log-in.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-in"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/log-out.svg b/src/pytaku/static/icons/log-out.svg
deleted file mode 100644
index 628a96d..0000000
--- a/src/pytaku/static/icons/log-out.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/search.svg b/src/pytaku/static/icons/search.svg
deleted file mode 100644
index c478d69..0000000
--- a/src/pytaku/static/icons/search.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
\ No newline at end of file
diff --git a/src/pytaku/static/icons/user.svg b/src/pytaku/static/icons/user.svg
deleted file mode 100644
index 40cd42f..0000000
--- a/src/pytaku/static/icons/user.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
\ No newline at end of file
diff --git a/src/pytaku/templates/base.html b/src/pytaku/templates/base.html
deleted file mode 100644
index d671d3c..0000000
--- a/src/pytaku/templates/base.html
+++ /dev/null
@@ -1,83 +0,0 @@
-{# vim: ft=htmldjango
-#}
-
-{% macro ibutton(href='', left_icon='', right_icon='', text='', color='red', title='', disabled=False) -%}
-{% set element = 'a' if href else 'button' %}
-<{{ element }} class="{{ color }} button {% if disabled %}disabled{% endif %}" title="{{title}}"
-        {% if href %}href="{{ href }}"{% endif %}
-        {% if disabled %}disabled{% endif %} >
-  {% if left_icon %}
-  <img src="{{ url_for('static', filename='icons/' + left_icon + '.svg')}}" alt="{{ text }} icon" />
-  {% endif %}
-
-  {% if text %}
-  <span>{{ text }}</span>
-  {% endif %}
-
-  {% if right_icon %}
-  <img src="{{ url_for('static', filename='icons/' + right_icon + '.svg')}}" alt="{{ text }} icon" />
-  {% endif %}
-</{{ element }}>
-{%- endmacro %}
-
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <title>
-      {% block title %}
-      {% endblock %}
-      - Pytaku
-    </title>
-    <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
-    <link rel="alternate icon" type="image/png" href="/static/favicon.png" />
-    <link rel="stylesheet" href="{{ url_for('static', filename='minireset.css') }}" />
-    <link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}" />
-
-    {% block head %}
-    {% endblock %}
-  </head>
-
-  <body>
-    <nav>
-      <a class="logo" href="/"><img src="{{ url_for('static', filename='pytaku.svg')}}" alt="home" /></a>
-      <form class="search" action="{{ url_for('search_view') }}" method="GET">
-        <input type="text" placeholder="search manga" name="q" value="{{ query }}" /><button class="red button">
-          <img src="{{ url_for('static', filename='icons/search.svg')}}" alt="search">
-        </button>
-      </form>
-      <span class="links">
-        {% if session.get('user') %}
-        <span>Hi there <strong>{{ session.user['username'] }}</strong>!</span>
-        <form method="POST" action="{{ url_for('logout_view') }}">
-          {{ ibutton(left_icon='log-out', text='Logout', color='gray') }}
-        </form>
-        {% else %}
-        <a href="/auth">
-          <img src="{{ url_for('static', filename='icons/log-in.svg')}}" alt="login/register" />
-          <span>Login/Register</span>
-        </a>
-        {% endif %}
-      </span>
-      </ul>
-    </nav>
-
-    <div class="content">
-    {% block content %}
-    {% endblock %}
-    </div>
-
-    <footer>
-      <p>Pytaku is <a href="https://git.sr.ht/~nhanb/pytaku">free and open source software</a>.</p>
-      <p>
-        <strong>This is a test instance.</strong>
-        Database may be wiped. Bugs may be present.<br>
-        Your favorite mangaka may never recover from his
-        <a href="https://junk.imnhan.com/hunter_x_idol.jpg">idol addiction</a>
-        and your favorite series may never finish.
-      </p>
-    </footer>
-  </body>
-
-</html>
diff --git a/src/pytaku/templates/old/auth.html b/src/pytaku/templates/old/auth.html
deleted file mode 100644
index 96b042a..0000000
--- a/src/pytaku/templates/old/auth.html
+++ /dev/null
@@ -1,67 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %}
-Login / Register
-{% endblock %}
-
-{% block head %}
-<style>
-.content {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  margin: auto;
-  max-width: 900px;
-  padding: 0;
-}
-
-.content > form {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  margin: 0 1rem;
-}
-
-.content input,
-.content button {
-  margin: .5rem 0;
-}
-
-.success {
-  color: inherit;
-}
-.error {
-  color: red;
-}
-
-</style>
-{% endblock %}
-
-{% block content %}
-<form method="POST">
-  <h1>Login</h1>
-  <input type="hidden" name="action" value="login">
-  <input name="username" placeholder="Username" required value="{{ login_username }}">
-  <input name="password" type="password" placeholder="Password" required value="{{ login_password }}">
-  <label for="remember">
-    <input name="remember" id="remember" type="checkbox" {% if login_remember %}checked{% endif %}>
-    Remember me
-  </label>
-  <button class="green button" type="submit">Login</button>
-  <p class="{{ 'error' if login_has_error else 'success'}}">
-  {{ login_message }}
-  </p>
-</form>
-
-<form method="POST">
-  <h1>Register</h1>
-  <input type="hidden" name="action" value="register">
-  <input name="username" placeholder="Username (2~15 chars)" required value="{{ register_username }}">
-  <input name="password" type="password" placeholder="Password (5~50 chars)" required value="{{ register_password }}">
-  <input name="confirm-password" type="password" placeholder="Confirm password" required value="{{ register_confirm_password }}">
-  <button class="blue button" type="submit">Register</button>
-  <p class="{{ 'error' if register_has_error else 'success'}}">
-  {{ register_message }}
-  </p>
-</form>
-{% endblock %}
diff --git a/src/pytaku/templates/old/chapter.html b/src/pytaku/templates/old/chapter.html
deleted file mode 100644
index 0ba18f8..0000000
--- a/src/pytaku/templates/old/chapter.html
+++ /dev/null
@@ -1,100 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %}
-Chapter {{ num_major }}{% if num_minor %}.{{ num_minor }}{% endif %}{% if name %} - {{ name }}{% endif %}
-{% endblock %}
-
-{% block head %}
-<meta property="og:title" content="{{ title['name'] }} {{ self.title() }}" />
-<meta property="og:image" content="{{ title['cover'] }}" />
-<meta property="og:description" content="{{ title['descriptions'] | join('\n') or '(no description)' }}" />
-
-{% if next_chapter %}
-<link rel="prefetch" href="{{ url_for('chapter_view', site=site, title_id=title_id, chapter_id=next_chapter['id'])}}">
-{% endif %}
-
-<style>
-  html {
-    background-color: #444;
-    color: white;
-  }
-
-  .content {
-    padding: var(--body-padding) 0;
-    text-align: center;
-  }
-
-  .cover {
-    width: 400px;
-    border: 1px solid black;
-  }
-
-  .pages img {
-    display: block;
-    margin: 0 auto .7rem auto;
-  }
-  .pages.webtoon img {
-    margin: 0 auto;
-  }
-
-  .buttons {
-    display: flex;
-    flex-direction: row;
-    justify-content: center;
-    flex-wrap: wrap;
-  }
-  .buttons > * {
-    margin-right: .5rem;
-    margin-top: .2rem;
-    margin-bottom: .2rem;
-  }
-  .buttons > *:last-child {
-    margin-right: 0;
-  }
-</style>
-
-{% endblock %}
-
-{% block content %}
-
-<h1>{{ self.title() }}</h1>
-
-{# Put buttons in block to reuse later in this same template #}
-{% block buttons %}
-<div class="buttons">
-  {% if prev_chapter %}
-  {{ ibutton(href=url_for('chapter_view', site=site, title_id=title_id, chapter_id=prev_chapter['id']), left_icon='chevrons-left', text='Prev') }}
-  {% else %}
-  {{ ibutton(left_icon='chevrons-left', text='Prev', disabled=True) }}
-  {% endif %}
-
-  {{ ibutton(href=url_for('title_view', title_id=title_id, site=site), left_icon='list', text='Chapter list', color='blue') }}
-
-  {% if next_chapter %}
-
-    {% set next_url = url_for('chapter_view', site=site, title_id=title_id, chapter_id=next_chapter['id']) %}
-    {% if session['user'] %}
-      {% set next_url = next_url + '?has_read=' + id %}
-    {% endif %}
-    {{ ibutton(href=next_url, right_icon='chevrons-right', text='Next') }}
-
-  {% else %}
-    {% if session['user'] %}
-    {{ ibutton(href=url_for('title_view', site=site, title_id=title_id) + '?has_read=' + id, text='✓ Finish reading', color='green') }}
-    {% else %}
-    {{ ibutton(right_icon='chevrons-right', text='Next', disabled=True) }}
-    {% endif %}
-  {% endif %}
-
-</div>
-{% endblock %}
-
-<div class="pages {% if is_webtoon %}webtoon{% endif %}">
-  {% for page in pages %}
-  <img src="{{ page }}" crossorigin="anonymous" referrerpolicy="no-referrer"/>
-  {% endfor %}
-</div>
-
-{{ self.buttons() }}
-
-{% endblock %}
diff --git a/src/pytaku/templates/old/follows.html b/src/pytaku/templates/old/follows.html
deleted file mode 100644
index 957bb24..0000000
--- a/src/pytaku/templates/old/follows.html
+++ /dev/null
@@ -1,104 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %}
-Stuff I follow
-{% endblock %}
-
-{% block head %}
-<style>
-.title {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  margin-bottom: var(--body-padding);
-  background-color: #efefef;
-}
-.title.empty {
-  display: inline-flex;
-}
-.title.empty .chapters {
-  display: none;
-}
-
-.cover {
-  border: 1px solid #777;
-  margin-right: .5rem;
-  max-width: 150px;
-}
-.cover:hover {
-  box-shadow: 0 0 3px black;
-}
-
-.chapters {
-  padding: .5rem .5rem .5rem 0;
-}
-
-.chapter {
-  display: block;
-  margin-bottom: .5rem;
-  background-color: white;
-  border: 1px solid #999;
-  padding: 6px;
-  border-radius: 5px;
-  text-decoration: none;
-  color: black;
-}
-.chapter:hover {
-  background-color: #eee;
-}
-.chapter:last-child::after {
-  content: '← resume here';
-  background-color: cornsilk;
-  white-space: nowrap;
-}
-
-.group {
-  font-size: .9em;
-  background-color: #ddd;
-  border-radius: 3px;
-  white-space: nowrap;
-  padding: 2px 5px;
-}
-
-.more {
-  display: inline-block;
-  font-style: italic;
-}
-</style>
-{% endblock %}
-
-{% block content %}
-
-{% for title in titles %}
-{% set title_url = url_for('title_view', site=title['site'], title_id=title['id']) %}
-<div class="title {% if title['chapters']|length == 0 %}empty{% endif %}">
-  <div>
-    <a href="{{ title_url }}">
-      <img class="cover"
-           src="{{ title['thumbnail'] }}"
-           alt="{{ title['name'] }}" />
-    </a>
-  </div>
-  <div class="chapters">
-    {% if title['chapters']|length > 4 %}
-    <a class="more chapter" href="{{ title_url }}">and {{ title['chapters']|length - 4 }} more...</a>
-    {% endif %}
-    {% for ch in title['chapters'][-4:] %}
-    <a class="chapter" href="{{ url_for('chapter_view', site=title['site'], title_id=title['id'], chapter_id=ch['id']) }}">
-      Chapter {{ ch['num_major'] }}{% if ch['num_minor'] %}.{{ ch['num_minor'] }}{% endif %}
-              {% if ch['volume'] %}Volume {{ ch['volume'] }} {% endif %}
-              {% if ch['name'] %} - {{ ch['name'] }}{% endif %}
-        {% for group in ch['groups'] %}
-        <span class="group">{{ group | truncate(20) }}</span>
-        {% endfor %}
-    </a>
-    {% endfor %}
-  </div>
-</div>
-{% endfor %}
-
-{% if not titles %}
-<p>You're not following any title yet. Try searching for some.</p>
-{% endif %}
-
-{% endblock %}
diff --git a/src/pytaku/templates/old/home.html b/src/pytaku/templates/old/home.html
deleted file mode 100644
index 92c7871..0000000
--- a/src/pytaku/templates/old/home.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %}
-Home
-{% endblock %}
-
-{% block content %}
-<p>Try searching for some manga title using the box above.</p>
-<p>Logging in allows you to follow manga titles.</p>
-{% endblock %}
diff --git a/src/pytaku/templates/old/import.html b/src/pytaku/templates/old/import.html
deleted file mode 100644
index 8734d98..0000000
--- a/src/pytaku/templates/old/import.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %}
-Import followed titles
-{% endblock %}
-
-{% block head %}
-<style>
-input[type=file] {
-  border: 2px solid black;
-  border-radius: var(--border-radius);
-  margin: 1rem 0;
-}
-
-.message {
-  color: red;
-}
-</style>
-{% endblock %}
-
-{% block content %}
-<h1>Importing from Tachiyomi</h1>
-
-<form method="POST" enctype="multipart/form-data" class="upload-form">
-  <p>Go to <b>Settings > Backup > Create backup</b>, then upload the generated json file here:</p>
-  <input type="file" name="tachiyomi"><br>
-  {{ ibutton(text='Submit') }}
-</form>
-
-{% with messages = get_flashed_messages() %}
-  {% for message in messages %}
-  <p class="message">{{ message }}</p>
-  {% endfor %}
-{% endwith %}
-
-{% endblock %}
diff --git a/src/pytaku/templates/old/search.html b/src/pytaku/templates/old/search.html
deleted file mode 100644
index 1e9b23a..0000000
--- a/src/pytaku/templates/old/search.html
+++ /dev/null
@@ -1,73 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %}
-  {% if query %}
-    "{{ query }}" search results
-  {% else %}
-    Search
-  {% endif %}
-{% endblock %}
-
-{% block head %}
-<style>
-  .site-heading {
-    text-transform: capitalize;
-  }
-  .results {
-    display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
-  }
-  .result {
-    display: inline-flex;
-    flex-direction: column;
-    border: 1px solid var(--bg-black);
-    margin: 0 1rem 1rem 0;
-    background-color: var(--bg-black);
-    color: white;
-    text-decoration: none;
-    max-width: 150px;
-  }
-  .result span {
-    padding: .5rem;
-    width: 0;
-    min-width: 100%;
-  }
-
-  .result-text {
-    margin-bottom: 1rem;
-  }
-</style>
-{% endblock %}
-
-{% block content %}
-
-{% if not query %}
-  <h1>Please enter a search query above.</h1>
-
-{% else %}
-{% for site, titles in results.items() %}
-<div>
-  <h1 class="site-heading">{{ site }}</h1>
-
-  {% if titles %}
-  <h2 class="result-text">Showing <strong>{{ titles | length }}</strong> result(s) for "{{ query }}":</h2>
-  {% else %}
-  <h2 class="result-text">No results for "{{ query }}".</h2>
-  {% endif %}
-
-  <div class="results">
-  {% for title in titles %}
-    <a class="result" href="{{ url_for('title_view', title_id=title['id'], site=title['site']) }}"
-       title="{{ title['name'] }}">
-      <img src="{{ title['thumbnail'] }}" alt="{{ title['name'] }}">
-      <span>{{ title['name'] | truncate(50) }}</span>
-    </a>
-  {% endfor %}
-  </div>
-</div>
-{% endfor %}
-{% endif %}
-
-
-{% endblock %}
diff --git a/src/pytaku/templates/old/title.html b/src/pytaku/templates/old/title.html
deleted file mode 100644
index d751de7..0000000
--- a/src/pytaku/templates/old/title.html
+++ /dev/null
@@ -1,83 +0,0 @@
-{% extends 'base.html' %}
-
-{% block head %}
-<meta property="og:title" content="{{ name }}" />
-<meta property="og:image" content="{{ cover }}" />
-<meta property="og:description" content="{{ descriptions | join('\n') or '(no description)' }}" />
-
-<style>
-  .cover {
-    width: 400px;
-    border: 1px solid black;
-  }
-
-  .details > form {
-    display: inline-block;
-  }
-</style>
-{% endblock %}
-
-{% block title %}
-{{ name }}
-{% endblock %}
-
-{% block content %}
-
-<div class="details">
-  <h1>{{ name }}</h1>
-  {% if session['user'] %}
-    {% if is_following %}
-      {% set fview = 'unfollow_view' %}
-      {% set ftext = 'Following' %}
-      {% set fcolor = 'red' %}
-      {% set ftitle = 'Click to unfollow' %}
-    {% else %}
-      {% set fview = 'follow_view' %}
-      {% set ftext = 'Follow' %}
-      {% set fcolor = 'green' %}
-      {% set ftitle = 'Click to follow' %}
-    {% endif %}
-    <form action="{{ url_for(fview, site=site, title_id=id) }}" method="POST">
-      {{ ibutton(left_icon='bookmark', text=ftext, color=fcolor, title=ftitle) }}
-    </form>
-  {% endif %}
-  {{ ibutton(href=source_url, right_icon='arrow-up-right', text=site|capitalize, color='blue', title='Go to source site') }}
-</div>
-
-<img class="cover" src="{{ cover }}" alt="cover" />
-{% for desc in descriptions %}
-<p class="description">{{ desc }}</p>
-{% endfor %}
-
-{% if chapters %}
-<table>
-  <tr>
-    {% if session['user'] %}<th>Done</th>{% endif %}
-    <th>Name</th>
-    <th>Group</th>
-  </tr>
-  {% for chapter in chapters %}
-  <tr>
-    {% if session['user'] %}
-    <td>
-      {% if chapter['is_read'] %}
-      {{ ibutton(href='?unread=' + chapter['id'], text='✓', color='green', title='Click to unread') }}
-      {% endif %}
-    </td>
-    {% endif %}
-    <td>
-      <a href="{{ url_for('chapter_view', chapter_id=chapter['id'], title_id=id, site=site) }}">
-        Chapter {{ chapter['number'] }}
-        {% if chapter['volume'] %}Volume {{ chapter['volume'] }} {% endif %}
-        {% if chapter['name'] %}- {{ chapter['name'] }} {% endif %}
-      </a>
-    </td>
-    <td>{{ ', '.join(chapter['groups']) }}</td>
-  </tr>
-  {% endfor %}
-</table>
-{% else %}
-<p>This one has no chapters.</p>
-{% endif %}
-
-{% endblock %}