Browse Source

Merge pull request #460 from cdvv7788/view_refactor

View refactor
Cristian Vargas 5 years ago
parent
commit
2767155e59

+ 1 - 0
archivebox/config/__init__.py

@@ -69,6 +69,7 @@ CONFIG_DEFAULTS: Dict[str, ConfigDefaultDict] = {
         'DEBUG':                    {'type': bool,  'default': False},
         'DEBUG':                    {'type': bool,  'default': False},
         'PUBLIC_INDEX':             {'type': bool,  'default': True},
         'PUBLIC_INDEX':             {'type': bool,  'default': True},
         'PUBLIC_SNAPSHOTS':         {'type': bool,  'default': True},
         'PUBLIC_SNAPSHOTS':         {'type': bool,  'default': True},
+        'PUBLIC_ADD_VIEW':          {'type': bool,  'default': False},
         'FOOTER_INFO':              {'type': str,   'default': 'Content is hosted for personal archiving purposes only.  Contact server owner for any takedown requests.'},
         'FOOTER_INFO':              {'type': str,   'default': 'Content is hosted for personal archiving purposes only.  Contact server owner for any takedown requests.'},
         'ACTIVE_THEME':             {'type': str,   'default': 'default'},
         'ACTIVE_THEME':             {'type': str,   'default': 'default'},
     },
     },

+ 2 - 29
archivebox/core/admin.py

@@ -2,7 +2,6 @@ __package__ = 'archivebox.core'
 
 
 from io import StringIO
 from io import StringIO
 from contextlib import redirect_stdout
 from contextlib import redirect_stdout
-from pathlib import Path
 
 
 from django.contrib import admin
 from django.contrib import admin
 from django.urls import path
 from django.urls import path
@@ -13,6 +12,7 @@ from django.contrib.auth import get_user_model
 
 
 from core.models import Snapshot
 from core.models import Snapshot
 from core.forms import AddLinkForm
 from core.forms import AddLinkForm
+from core.utils import get_icons
 
 
 from util import htmldecode, urldecode, ansi_to_html
 from util import htmldecode, urldecode, ansi_to_html
 from logging_util import printable_filesize
 from logging_util import printable_filesize
@@ -93,34 +93,7 @@ class SnapshotAdmin(admin.ModelAdmin):
         ) + mark_safe(f'<span class="tags">{tags}</span>')
         ) + mark_safe(f'<span class="tags">{tags}</span>')
 
 
     def files(self, obj):
     def files(self, obj):
-        link = obj.as_link()
-        canon = link.canonical_outputs()
-        out_dir = Path(link.link_dir)
-
-        link_tuple = lambda link, method: (link.archive_path, canon[method] or '', canon[method] and (out_dir / (canon[method] or 'notdone')).exists())
-
-        return format_html(
-            '<span class="files-icons" style="font-size: 1.2em; opacity: 0.8">'
-                '<a href="/{}/{}/" class="exists-{}" title="Wget clone">🌐 </a> '
-                '<a href="/{}/{}" class="exists-{}" title="PDF">📄</a> '
-                '<a href="/{}/{}" class="exists-{}" title="Screenshot">🖥 </a> '
-                '<a href="/{}/{}" class="exists-{}" title="HTML dump">🅷 </a> '
-                '<a href="/{}/{}/" class="exists-{}" title="WARC">🆆 </a> '
-                '<a href="/{}/{}" class="exists-{}" title="SingleFile">&#128476; </a>'
-                '<a href="/{}/{}/" class="exists-{}" title="Media files">📼 </a> '
-                '<a href="/{}/{}/" class="exists-{}" title="Git repos">📦 </a> '
-                '<a href="{}" class="exists-{}" title="Archive.org snapshot">🏛 </a> '
-            '</span>',
-            *link_tuple(link, 'wget_path'),
-            *link_tuple(link, 'pdf_path'),
-            *link_tuple(link, 'screenshot_path'),
-            *link_tuple(link, 'dom_path'),
-            *link_tuple(link, 'warc_path')[:2], any((out_dir / canon['warc_path']).glob('*.warc.gz')),
-            *link_tuple(link, 'singlefile_path'),
-            *link_tuple(link, 'media_path')[:2], any((out_dir / canon['media_path']).glob('*')),
-            *link_tuple(link, 'git_path')[:2], any((out_dir / canon['git_path']).glob('*')),
-            canon['archive_org_path'], (out_dir / 'archive.org.txt').exists(),
-        )
+        return get_icons(obj)
 
 
     def size(self, obj):
     def size(self, obj):
         return format_html(
         return format_html(

+ 5 - 3
archivebox/core/urls.py

@@ -5,7 +5,7 @@ from django.views import static
 from django.conf import settings
 from django.conf import settings
 from django.views.generic.base import RedirectView
 from django.views.generic.base import RedirectView
 
 
-from core.views import MainIndex, OldIndex, LinkDetails
+from core.views import MainIndex, LinkDetails, PublicArchiveView, AddView
 
 
 
 
 # print('DEBUG', settings.DEBUG)
 # print('DEBUG', settings.DEBUG)
@@ -18,7 +18,9 @@ urlpatterns = [
 
 
     path('archive/', RedirectView.as_view(url='/')),
     path('archive/', RedirectView.as_view(url='/')),
     path('archive/<path:path>', LinkDetails.as_view(), name='LinkAssets'),
     path('archive/<path:path>', LinkDetails.as_view(), name='LinkAssets'),
-    path('add/', RedirectView.as_view(url='/admin/core/snapshot/add/')),
+
+    path('admin/core/snapshot/add/', RedirectView.as_view(url='/add/')),
+    path('add/', AddView.as_view()),
     
     
     path('accounts/login/', RedirectView.as_view(url='/admin/login/')),
     path('accounts/login/', RedirectView.as_view(url='/admin/login/')),
     path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')),
     path('accounts/logout/', RedirectView.as_view(url='/admin/logout/')),
@@ -27,8 +29,8 @@ urlpatterns = [
     path('accounts/', include('django.contrib.auth.urls')),
     path('accounts/', include('django.contrib.auth.urls')),
     path('admin/', admin.site.urls),
     path('admin/', admin.site.urls),
     
     
-    path('old.html', OldIndex.as_view(), name='OldHome'),
     path('index.html', RedirectView.as_view(url='/')),
     path('index.html', RedirectView.as_view(url='/')),
     path('index.json', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'index.json'}),
     path('index.json', static.serve, {'document_root': settings.OUTPUT_DIR, 'path': 'index.json'}),
     path('', MainIndex.as_view(), name='Home'),
     path('', MainIndex.as_view(), name='Home'),
+    path('public/', PublicArchiveView.as_view(), name='public-index'),
 ]
 ]

+ 36 - 0
archivebox/core/utils.py

@@ -0,0 +1,36 @@
+from pathlib import Path
+
+from django.utils.html import format_html
+
+from core.models import Snapshot
+
+
+def get_icons(snapshot: Snapshot) -> str:
+    link = snapshot.as_link()
+    canon = link.canonical_outputs()
+    out_dir = Path(link.link_dir)
+
+    link_tuple = lambda link, method: (link.archive_path, canon[method] or '', canon[method] and (out_dir / (canon[method] or 'notdone')).exists())
+
+    return format_html(
+            '<span class="files-icons" style="font-size: 1.2em; opacity: 0.8">'
+                '<a href="/{}/{}/" class="exists-{}" title="Wget clone">🌐 </a> '
+                '<a href="/{}/{}" class="exists-{}" title="PDF">📄</a> '
+                '<a href="/{}/{}" class="exists-{}" title="Screenshot">🖥 </a> '
+                '<a href="/{}/{}" class="exists-{}" title="HTML dump">🅷 </a> '
+                '<a href="/{}/{}/" class="exists-{}" title="WARC">🆆 </a> '
+                '<a href="/{}/{}" class="exists-{}" title="SingleFile">&#128476; </a>'
+                '<a href="/{}/{}/" class="exists-{}" title="Media files">📼 </a> '
+                '<a href="/{}/{}/" class="exists-{}" title="Git repos">📦 </a> '
+                '<a href="{}" class="exists-{}" title="Archive.org snapshot">🏛 </a> '
+            '</span>',
+            *link_tuple(link, 'wget_path'),
+            *link_tuple(link, 'pdf_path'),
+            *link_tuple(link, 'screenshot_path'),
+            *link_tuple(link, 'dom_path'),
+            *link_tuple(link, 'warc_path')[:2], any((out_dir / canon['warc_path']).glob('*.warc.gz')),
+            *link_tuple(link, 'singlefile_path'),
+            *link_tuple(link, 'media_path')[:2], any((out_dir / canon['media_path']).glob('*')),
+            *link_tuple(link, 'git_path')[:2], any((out_dir / canon['git_path']).glob('*')),
+            canon['archive_org_path'], (out_dir / 'archive.org.txt').exists(),
+        )

+ 69 - 27
archivebox/core/views.py

@@ -1,21 +1,28 @@
 __package__ = 'archivebox.core'
 __package__ = 'archivebox.core'
 
 
+from io import StringIO
+from contextlib import redirect_stdout
+
 from django.shortcuts import render, redirect
 from django.shortcuts import render, redirect
 
 
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.views import View, static
 from django.views import View, static
+from django.views.generic.list import ListView
+from django.views.generic import FormView
+from django.contrib.auth.mixins import UserPassesTestMixin
 
 
 from core.models import Snapshot
 from core.models import Snapshot
+from core.utils import get_icons
+from core.forms import AddLinkForm
 
 
-from ..index import load_main_index, load_main_index_meta
 from ..config import (
 from ..config import (
     OUTPUT_DIR,
     OUTPUT_DIR,
-    VERSION,
-    FOOTER_INFO,
     PUBLIC_INDEX,
     PUBLIC_INDEX,
     PUBLIC_SNAPSHOTS,
     PUBLIC_SNAPSHOTS,
+    PUBLIC_ADD_VIEW
 )
 )
-from ..util import base_url
+from main import add
+from ..util import base_url, ansi_to_html
 
 
 
 
 class MainIndex(View):
 class MainIndex(View):
@@ -26,32 +33,10 @@ class MainIndex(View):
             return redirect('/admin/core/snapshot/')
             return redirect('/admin/core/snapshot/')
 
 
         if PUBLIC_INDEX:
         if PUBLIC_INDEX:
-            return redirect('OldHome')
+            return redirect('public-index')
         
         
         return redirect(f'/admin/login/?next={request.path}')
         return redirect(f'/admin/login/?next={request.path}')
 
 
-        
-
-class OldIndex(View):
-    template = 'main_index.html'
-
-    def get(self, request):
-        if PUBLIC_INDEX or request.user.is_authenticated:
-            all_links = load_main_index(out_dir=OUTPUT_DIR)
-            meta_info = load_main_index_meta(out_dir=OUTPUT_DIR)
-
-            context = {
-                'updated': meta_info['updated'],
-                'num_links': meta_info['num_links'],
-                'links': all_links,
-                'VERSION': VERSION,
-                'FOOTER_INFO': FOOTER_INFO,
-            }
-
-            return render(template_name=self.template, request=request, context=context)
-
-        return redirect(f'/admin/login/?next={request.path}')
-
 
 
 class LinkDetails(View):
 class LinkDetails(View):
     def get(self, request, path):
     def get(self, request, path):
@@ -102,3 +87,60 @@ class LinkDetails(View):
             content_type="text/plain",
             content_type="text/plain",
             status=404,
             status=404,
         )
         )
+
+class PublicArchiveView(ListView):
+    template = 'snapshot_list.html'
+    model = Snapshot
+    paginate_by = 100
+
+    def get_queryset(self, **kwargs): 
+        qs = super().get_queryset(**kwargs) 
+        query = self.request.GET.get('q')
+        if query:
+            qs = Snapshot.objects.filter(title__icontains=query)
+        for snapshot in qs:
+            snapshot.icons = get_icons(snapshot) 
+        return qs
+
+    def get(self, *args, **kwargs):
+        if PUBLIC_INDEX or self.request.user.is_authenticated:
+            response = super().get(*args, **kwargs)
+            return response
+        else:
+            return redirect(f'/admin/login/?next={self.request.path}')
+
+
+class AddView(UserPassesTestMixin, FormView):
+    template_name = "add_links.html"
+    form_class = AddLinkForm
+
+    def test_func(self):
+        return PUBLIC_ADD_VIEW or self.request.user.is_authenticated
+
+    def get_context_data(self, *args, **kwargs):
+        context = super().get_context_data(*args, **kwargs)
+        context["title"] = "Add URLs"
+        return context
+
+    def form_valid(self, form):
+        url = form.cleaned_data["url"]
+        print(f'[+] Adding URL: {url}')
+        depth = 0 if form.cleaned_data["depth"] == "0" else 1
+        input_kwargs = {
+            "urls": url,
+            "depth": depth,
+            "update_all": False,
+            "out_dir": OUTPUT_DIR,
+        }
+        add_stdout = StringIO()
+        with redirect_stdout(add_stdout):
+            add(**input_kwargs)
+            print(add_stdout.getvalue())
+
+        context = self.get_context_data()
+
+        context.update({
+            "stdout": ansi_to_html(add_stdout.getvalue().strip()),
+            "form": AddLinkForm()
+        })
+        return render(template_name=self.template_name, request=self.request, context=context)

+ 0 - 1
archivebox/themes/admin/base.html

@@ -89,7 +89,6 @@
             <a href="{% url 'admin:Add' %}">Add ➕</a> /
             <a href="{% url 'admin:Add' %}">Add ➕</a> /
             <a href="{% url 'Home' %}">Snapshots</a> /
             <a href="{% url 'Home' %}">Snapshots</a> /
             <a href="/admin/auth/user/">Users</a> /
             <a href="/admin/auth/user/">Users</a> /
-            <a href="{% url 'OldHome' %}">Old UI</a> /
             <a href="{% url 'Docs' %}">Docs</a>
             <a href="{% url 'Docs' %}">Docs</a>
              &nbsp; &nbsp;
              &nbsp; &nbsp;
             {% block welcome-msg %}
             {% block welcome-msg %}

+ 9 - 44
archivebox/themes/default/add_links.html

@@ -1,4 +1,6 @@
-{% extends "admin/index.html" %}
+{% extends "base.html" %}
+
+{% load static %}
 {% load i18n %}
 {% load i18n %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
@@ -8,48 +10,11 @@
     </div>
     </div>
 {% endblock %}
 {% endblock %}
 
 
-{% block content %}
-    <style>
-        .dashboard #content {
-            width: 100%;
-            margin-right: 0px;
-            margin-left: 0px;
-        }
-        #submit {
-            border: 1px solid rgba(0,0,0,0.2);
-            padding: 10px;
-            border-radius: 4px;
-            background-color: #f5dd5d;
-            color: #333;
-            font-size: 18px;
-            font-weight: 800;
-        }
-        #add-form button[role=submit]:hover {
-            background-color: #e5cd4d;
-        }
-        #add-form label {
-            display: block;
-            font-size: 16px;
-        }
-        #add-form textarea {
-            width: 100%;
-            min-height: 300px;
-        }
-        #delay-warning div {
-            border: 1px solid red;
-            border-radius: 4px;
-            margin: 10px;
-            padding: 10px;
-            font-size: 15px;
-            background-color: #F5DD5D;
-        }
-        #stdout {
-            background-color: #ded;
-            padding: 10px 10px;
-            border-radius: 4px;
-            white-space: normal;
-        }
-    </style>
+{% block extra_head %}
+    <link rel="stylesheet" href="{% static 'add.css' %}" />
+{% endblock %}
+
+{% block body %}
     <div style="max-width: 550px; margin: auto; float: none">
     <div style="max-width: 550px; margin: auto; float: none">
         <br/><br/>
         <br/><br/>
         {% if stdout %}
         {% if stdout %}
@@ -63,7 +28,7 @@
                 <a href="/add" id="submit">&nbsp; Add more URLs ➕</a>
                 <a href="/add" id="submit">&nbsp; Add more URLs ➕</a>
             </center>
             </center>
         {% else %}
         {% else %}
-            <form id="add-form" action="?" method="POST" class="p-form">{% csrf_token %}
+            <form id="add-form" method="POST" class="p-form">{% csrf_token %}
                 <h1>Add new URLs to your archive</h1>
                 <h1>Add new URLs to your archive</h1>
                 <br/>
                 <br/>
                 {{ form.as_p }}
                 {{ form.as_p }}

+ 286 - 0
archivebox/themes/default/base.html

@@ -0,0 +1,286 @@
+{% load static %}
+
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <title>Archived Sites</title>
+    <meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1">
+
+    <style>
+        :root {
+            --bg-main: #efefef;
+            --accent-1: #aa1e55;
+            --accent-2: #ffebeb;
+            --accent-3: #efefef;
+            
+            --text-1: #1c1c1c;
+            --text-2: #eaeaea;
+            --text-main: #1a1a1a;
+            --font-main: "Gill Sans", Helvetica, sans-serif;
+        }
+        
+        /* Dark Mode (WIP) */
+        /*
+        @media (prefers-color-scheme: dark) {
+            :root {
+                --accent-2: hsl(160, 100%, 96%);
+                
+                --text-1: #eaeaea;
+                --text-2: #1a1a1a;
+                --bg-main: #101010;
+            }
+            
+            #table-bookmarks_wrapper,
+            #table-bookmarks_wrapper img,
+            tbody td:nth-child(3),
+            tbody td:nth-child(3) span,
+            footer {
+                filter: invert(100%);
+            }
+        }*/
+        
+        html,
+        body {
+            width: 100%;
+            height: 100%;
+            font-size: 18px;
+            font-weight: 200;
+            text-align: center;
+            margin: 0px;
+            padding: 0px;
+            font-family: var(--font-main);
+        }
+        
+        .header-top small {
+            font-weight: 200;
+            color: var(--accent-3);
+        }
+        
+        .header-top {
+            width: 100%;
+            height: auto;
+            min-height: 40px;
+            margin: 0px;
+            text-align: center;
+            color: white;
+            font-size: calc(11px + 0.84vw);
+            font-weight: 200;
+            padding: 4px 4px;
+            border-bottom: 3px solid var(--accent-1);
+            background-color: var(--accent-1);
+        }
+        
+        input[type=search] {
+            width: 22vw;
+            border-radius: 4px;
+            border: 1px solid #aeaeae;
+            padding: 3px 5px;
+        }
+        
+        .nav>div {
+            min-height: 30px;
+        }
+        
+        .header-top a {
+            text-decoration: none;
+            color: rgba(0, 0, 0, 0.6);
+        }
+        
+        .header-top a:hover {
+            text-decoration: none;
+            color: rgba(0, 0, 0, 0.9);
+        }
+        
+        .header-top .col-lg-4 {
+            text-align: center;
+            padding-top: 4px;
+            padding-bottom: 4px;
+        }
+        
+        .header-archivebox img {
+            display: inline-block;
+            margin-right: 3px;
+            height: 30px;
+            margin-left: 12px;
+            margin-top: -4px;
+            margin-bottom: 2px;
+        }
+        
+        .header-archivebox img:hover {
+            opacity: 0.5;
+        }
+        
+        #table-bookmarks_length,
+        #table-bookmarks_filter {
+            padding-top: 12px;
+            opacity: 0.8;
+            padding-left: 24px;
+            padding-right: 22px;
+            margin-bottom: -16px;
+        }
+        
+        table {
+            padding: 6px;
+            width: 100%;
+        }
+        
+        table thead th {
+            font-weight: 400;
+        }
+        
+        table tr {
+            height: 35px;
+        }
+        
+        tbody tr:nth-child(odd) {
+            background-color: var(--accent-2) !important;
+        }
+        
+        table tr td {
+            white-space: nowrap;
+            overflow: hidden;
+            /*padding-bottom: 0.4em;*/
+            /*padding-top: 0.4em;*/
+            padding-left: 2px;
+            text-align: center;
+        }
+        
+        table tr td a {
+            text-decoration: none;
+        }
+        
+        table tr td img,
+        table tr td object {
+            display: inline-block;
+            margin: auto;
+            height: 24px;
+            width: 24px;
+            padding: 0px;
+            padding-right: 5px;
+            vertical-align: middle;
+            margin-left: 4px;
+        }
+        
+        #table-bookmarks {
+            width: 100%;
+            overflow-y: scroll;
+            table-layout: fixed;
+        }
+        
+        .dataTables_wrapper {
+            background-color: #fafafa;
+        }
+        
+        table tr a span[data-archived~=False] {
+            opacity: 0.4;
+        }
+        
+        .files-spinner {
+            height: 15px;
+            width: auto;
+            opacity: 0.5;
+            vertical-align: -2px;
+        }
+        
+        .in-progress {
+            display: none;
+        }
+        
+        body[data-status~=finished] .files-spinner {
+            display: none;
+        }
+        
+        /*body[data-status~=running] .in-progress {
+            display: inline-block;
+        }*/
+        tr td a.favicon img {
+            padding-left: 6px;
+            padding-right: 12px;
+            vertical-align: -4px;
+        }
+        
+        tr td a.title {
+            font-size: 1.4em;
+            text-decoration: none;
+            color: black;
+        }
+        
+        tr td a.title small {
+            background-color: var(--accent-3);
+            border-radius: 4px;
+            float: right
+        }
+        
+        input[type=search]::-webkit-search-cancel-button {
+            -webkit-appearance: searchfield-cancel-button;
+        }
+        
+        .title-col {
+            text-align: left;
+        }
+        
+        .title-col a {
+            color: black;
+        }
+    </style>
+    <link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
+    <link rel="stylesheet" href="{% static 'jquery.dataTables.min.css' %}" />
+            {% block extra_head %}
+            {% endblock %}
+    <script src="{% static 'jquery.min.js' %}"></script>
+    <script src="{% static 'jquery.dataTables.min.js' %}"></script>
+    <script>
+        document.addEventListener('error', function (e) {
+            e.target.style.opacity = 0;
+        }, true)
+        jQuery(document).ready(function () {
+            jQuery('#table-bookmarks').DataTable({
+                searching: false,
+                paging: false,
+                stateSave: true, // save state (filtered input, number of entries shown, etc) in localStorage
+                dom: '<lf<t>ip>', // how to show the table and its helpers (filter, etc) in the DOM
+                    order: [[0, 'desc']],
+                    iDisplayLength: 100,
+                });
+            });
+        </script>
+        <base href="{% url 'Home' %}">
+    </head>
+    <body>
+        <header>
+            <div class="header-top container-fluid">
+                <div class="row nav">
+                    <div class="col-sm-2">
+                        <a href="{% url 'public-index' %}" class="header-archivebox" title="Last updated: {{updated}}">
+                            <img src="{% static 'archive.png' %}" alt="Logo" />
+                            ArchiveBox: Index
+                        </a>
+                    </div>
+                    <div class="col-sm-10" style="text-align: right">
+                        <a href="/add/">Add Links</a> &nbsp; | &nbsp;
+                        <a href="/admin/core/snapshot/">Admin</a> &nbsp; | &nbsp;
+                        <a href="https://github.com/pirate/ArchiveBox/wiki">Docs</a>
+                    </div>
+                </div>
+            </div>
+        </header>
+        {% block body %}
+        {% endblock %}
+        <br>
+        <footer>
+            <br />
+            <center>
+                <small>
+                    Archive created using <a href="https://github.com/pirate/ArchiveBox" title="Github">ArchiveBox</a> &nbsp; |
+                    &nbsp;
+                    Download index as <a href="index.json" title="JSON summary of archived links.">JSON</a>
+                    <br /><br />
+                    {{FOOTER_INFO}}
+                </small>
+            </center>
+            <br />
+        </footer>
+    </body>
+    
+    </html>

+ 64 - 0
archivebox/themes/default/core/snapshot_list.html

@@ -0,0 +1,64 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block body %}
+<br>
+        <form action="{% url 'public-index' %}" method="get">
+            <input name="q" type="text" placeholder="Search...">
+            <button type="submit">Search</button>
+            <button onclick="location.href='{% url 'public-index' %}'" type="button">
+                Reload Index</button>
+          </form>
+            <table id="table-bookmarks">
+            <thead>
+                <tr>
+                    <th style="width: 100px;">Bookmarked</th>
+                    <th style="width: 26vw;">Saved Link ({{num_links}})</th>
+                    <th style="width: 50px">Files</th>
+                    <th style="width: 16vw;whitespace:nowrap;overflow-x:hidden;">Original URL</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for link in object_list %}
+                    <tr>
+                        <td title="{{link.timestamp}}">{{link.added}}</td>
+                        <td class="title-col">
+                            {% if link.is_archived %}
+                                <a href="archive/{{link.timestamp}}/index.html"><img src="archive/{{link.timestamp}}/favicon.ico" class="link-favicon" decoding="async"></a>
+                            {% else %}
+                                <a href="archive/{{link.timestamp}}/index.html"><img src="{% static 'spinner.gif' %}" class="link-favicon" decoding="async"></a>
+                            {% endif %}
+                            <a href="archive/{{link.timestamp}}/index.html" title="{{link.title}}">
+                                <span data-title-for="{{link.url}}" data-archived="{{link.is_archived}}">{{link.title|default:'Loading...'}}</span>
+                                <small style="float:right">{{link.tags|default:''}}</small>
+                            </a>
+                        </td>
+                        <td>
+                            <a href="archive/{{link.timestamp}}/index.html">📄 
+                                <span data-number-for="{{link.url}}" title="Fetching any missing files...">{{link.icons}} <img src="{% static 'spinner.gif' %}" class="files-spinner" decoding="async"/></span>
+                            </a>
+                        </td>
+                        <td style="text-align:left"><a href="{{link.url}}">{{link.url}}</a></td>
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+        <center>
+            <span class="step-links">
+                {% if page_obj.has_previous %}
+                    <a href="{% url 'public-index' %}?page=1">&laquo; first</a>
+                    <a href="{% url 'public-index' %}?page={{ page_obj.previous_page_number }}">previous</a>
+                {% endif %}
+        
+                <span class="current">
+                    Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
+                </span>
+        
+                {% if page_obj.has_next %}
+                    <a href="{% url 'public-index' %}?page={{ page_obj.next_page_number }}">next </a>
+                    <a href="{% url 'public-index' %}?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
+                {% endif %}
+            </span>
+            <br>
+    </center>
+        {% endblock %}

+ 62 - 0
archivebox/themes/default/static/add.css

@@ -0,0 +1,62 @@
+.dashboard #content {
+  width: 100%;
+  margin-right: 0px;
+  margin-left: 0px;
+}
+#submit {
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  padding: 10px;
+  border-radius: 4px;
+  background-color: #f5dd5d;
+  color: #333;
+  font-size: 18px;
+  font-weight: 800;
+}
+#add-form button[role="submit"]:hover {
+  background-color: #e5cd4d;
+}
+#add-form label {
+  display: block;
+  font-size: 16px;
+}
+#add-form textarea {
+  width: 100%;
+  min-height: 300px;
+}
+#delay-warning div {
+  border: 1px solid red;
+  border-radius: 4px;
+  margin: 10px;
+  padding: 10px;
+  font-size: 15px;
+  background-color: #f5dd5d;
+}
+#stdout {
+  background-color: #ded;
+  padding: 10px 10px;
+  border-radius: 4px;
+  white-space: normal;
+}
+ul#id_depth {
+  list-style-type: none;
+  padding: 0;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.loader {
+  border: 16px solid #f3f3f3; /* Light grey */
+  border-top: 16px solid #3498db; /* Blue */
+  border-radius: 50%;
+  width: 30px;
+  height: 30px;
+  box-sizing: border-box;
+  animation: spin 2s linear infinite;
+}