Compare commits
2 commits
002a8cc543
...
56d6c8e408
| Author | SHA1 | Date | |
|---|---|---|---|
| 56d6c8e408 | |||
| c16fdde19a |
4 changed files with 203 additions and 34 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import calendar
|
import calendar
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import html.parser
|
import html.parser
|
||||||
|
|
@ -132,6 +133,9 @@ class Entry:
|
||||||
link=link,
|
link=link,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def posted_time_iso(self) -> str:
|
||||||
|
return datetime.datetime.fromtimestamp(self.posted_at / 1000).isoformat()
|
||||||
|
|
||||||
def time_ago(self) -> str:
|
def time_ago(self) -> str:
|
||||||
posted = int(self.posted_at / 1000)
|
posted = int(self.posted_at / 1000)
|
||||||
seconds = int(time.time()) - posted
|
seconds = int(time.time()) - posted
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
body {
|
body {
|
||||||
|
box-sizing: border-box;
|
||||||
margin-right: 4rem;
|
margin-right: 4rem;
|
||||||
margin-left: 4rem;
|
margin-left: 4rem;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,3 +40,92 @@ li.entry:before {
|
||||||
content: '\2022';
|
content: '\2022';
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FEED VIEW
|
||||||
|
*/
|
||||||
|
.fv-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-title {
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-subscribe {
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-main-panel {
|
||||||
|
grid-row: 3;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-columns: 1fr 3fr;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-summary-panel {
|
||||||
|
grid-column: 1;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
|
||||||
|
margin-right: 1rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-summary-refresh {
|
||||||
|
grid-row: 1;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-summary-refresh input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-summary-list {
|
||||||
|
grid-row: 2;
|
||||||
|
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fv-feed-panel {
|
||||||
|
grid-column: 2;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FEED SUMMARY ITEM
|
||||||
|
*/
|
||||||
|
.feed-summary {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-summary-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-summary-timestamp {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-summary-entry {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
|
||||||
137
cry/views.py
137
cry/views.py
|
|
@ -4,6 +4,23 @@ import dominate.tags as tags
|
||||||
|
|
||||||
from . import feed
|
from . import feed
|
||||||
|
|
||||||
|
|
||||||
|
class time(tags.html_tag):
|
||||||
|
"""The <time> HTML element represents a specific period in time. It may
|
||||||
|
include the datetime attribute to translate dates into machine-readable
|
||||||
|
format, allowing for better search engine results or custom features such
|
||||||
|
as reminders.
|
||||||
|
|
||||||
|
It may represent one of the following:
|
||||||
|
|
||||||
|
- A time on a 24-hour clock.
|
||||||
|
- A precise date in the Gregorian calendar (with optional time and timezone information).
|
||||||
|
- A valid time duration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _standard_header(title: str) -> tags.head:
|
def _standard_header(title: str) -> tags.head:
|
||||||
head = tags.head(
|
head = tags.head(
|
||||||
tags.meta(charset="utf8"),
|
tags.meta(charset="utf8"),
|
||||||
|
|
@ -13,48 +30,104 @@ def _standard_header(title: str) -> tags.head:
|
||||||
assert isinstance(head, tags.head)
|
assert isinstance(head, tags.head)
|
||||||
return head
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def _feed_summary(f: feed.Feed) -> tags.li:
|
||||||
|
if len(f.entries) == 0:
|
||||||
|
last_entry = tags.p({"class": "feed-summary-entry"}, tags.i("Never posted"))
|
||||||
|
else:
|
||||||
|
entry = f.entries[0]
|
||||||
|
last_entry = [
|
||||||
|
tags.p(
|
||||||
|
{"class": "feed-summary-timestamp"},
|
||||||
|
tags.i(
|
||||||
|
f"Posted ",
|
||||||
|
time(entry.time_ago(), datetime=entry.posted_time_iso()),
|
||||||
|
" ago",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tags.p({"class": "feed-summary-entry"}, entry.title),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = tags.li(
|
||||||
|
{"class": "feed-summary"},
|
||||||
|
# TODO: Image!
|
||||||
|
tags.h2({"class": "feed-summary-title"}, f.title),
|
||||||
|
last_entry,
|
||||||
|
)
|
||||||
|
assert isinstance(result, tags.li)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def feed_view(feeds: list[feed.Feed]) -> tags.html:
|
def feed_view(feeds: list[feed.Feed]) -> tags.html:
|
||||||
document = tags.html(
|
document = tags.html(
|
||||||
_standard_header("Subscribed Feeds"),
|
_standard_header("Subscribed Feeds"),
|
||||||
tags.h1("Feeds"),
|
tags.body(
|
||||||
tags.div(
|
{"class": "fv-body"},
|
||||||
|
tags.h1({"class": "fv-title"}, "Feeds"),
|
||||||
tags.form(
|
tags.form(
|
||||||
{"method": "post", "action": "/refresh"},
|
{"class": "fv-subscribe", "method": "post", "action": "/subscribe"},
|
||||||
tags.input_(type="submit", value="Refresh"),
|
|
||||||
),
|
|
||||||
tags.form(
|
|
||||||
{"method": "post", "action": "/subscribe"},
|
|
||||||
tags.label({"for": "url"}, "Feed url:"),
|
tags.label({"for": "url"}, "Feed url:"),
|
||||||
tags.input_(type="url", name="url"),
|
tags.input_(type="url", name="url"),
|
||||||
tags.input_(type="submit", value="Subscribe"),
|
tags.input_(type="submit", value="Subscribe"),
|
||||||
),
|
),
|
||||||
(
|
tags.div(
|
||||||
|
{"class": "fv-main-panel"},
|
||||||
tags.div(
|
tags.div(
|
||||||
{"class": "feed"},
|
{"class": "fv-summary-panel"},
|
||||||
tags.h2(tags.a(f.title, href=f.link, target="_blank")),
|
tags.form(
|
||||||
(
|
{
|
||||||
tags.ul(
|
"class": "fv-summary-refresh",
|
||||||
tags.li(
|
"method": "post",
|
||||||
{"class": "entry"},
|
"action": "/refresh",
|
||||||
tags.a(
|
},
|
||||||
entry.title, href=entry.link, target="_blank"
|
tags.input_(type="submit", value="Refresh"),
|
||||||
),
|
|
||||||
f" ({entry.time_ago()})",
|
|
||||||
)
|
|
||||||
for entry in f.entries
|
|
||||||
)
|
|
||||||
if len(f.entries) > 0
|
|
||||||
else tags.i("No entries...")
|
|
||||||
),
|
),
|
||||||
)
|
tags.ul(
|
||||||
for f in feeds
|
{"class": "fv-summary-list"},
|
||||||
|
(_feed_summary(f) for f in feeds),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tags.div(
|
||||||
|
{"class": "fv-feed-panel"},
|
||||||
|
(
|
||||||
|
tags.div(
|
||||||
|
{"class": "fv-feed"},
|
||||||
|
tags.h2(
|
||||||
|
{"class": "fv-feed-title"},
|
||||||
|
tags.a(f.title, href=f.link, target="_blank"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
tags.ul(
|
||||||
|
tags.li(
|
||||||
|
{"class": "entry"},
|
||||||
|
tags.a(
|
||||||
|
entry.title,
|
||||||
|
href=entry.link,
|
||||||
|
target="_blank",
|
||||||
|
),
|
||||||
|
" ",
|
||||||
|
time(
|
||||||
|
f"({entry.time_ago()})",
|
||||||
|
datetime=entry.posted_time_iso(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for entry in f.entries
|
||||||
|
)
|
||||||
|
if len(f.entries) > 0
|
||||||
|
else tags.i("No entries...")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for f in feeds
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert isinstance(document, tags.html)
|
assert isinstance(document, tags.html)
|
||||||
return document
|
return document
|
||||||
|
|
||||||
def subscribe_choose_view(candidates: typing.Iterable[tuple[str,str]]) -> tags.html:
|
|
||||||
|
def subscribe_choose_view(candidates: typing.Iterable[tuple[str, str]]) -> tags.html:
|
||||||
document = tags.html(
|
document = tags.html(
|
||||||
_standard_header("Choose Feed"),
|
_standard_header("Choose Feed"),
|
||||||
tags.h1("Choose Feed"),
|
tags.h1("Choose Feed"),
|
||||||
|
|
@ -64,11 +137,15 @@ def subscribe_choose_view(candidates: typing.Iterable[tuple[str,str]]) -> tags.h
|
||||||
tags.thead(tags.tr(tags.th("Title"), tags.th("URL"), tags.th())),
|
tags.thead(tags.tr(tags.th("Title"), tags.th("URL"), tags.th())),
|
||||||
tags.tbody(
|
tags.tbody(
|
||||||
tags.form(
|
tags.form(
|
||||||
{"action": "/subscribe", "method":"post"},
|
{"action": "/subscribe", "method": "post"},
|
||||||
tags.tr(
|
tags.tr(
|
||||||
tags.td(title), tags.td(url), tags.td(
|
tags.td(title),
|
||||||
tags.input_({"type":"hidden", "name":"url", "value":url}),
|
tags.td(url),
|
||||||
tags.input_({"type":"submit", "value":"Subscribe"}),
|
tags.td(
|
||||||
|
tags.input_(
|
||||||
|
{"type": "hidden", "name": "url", "value": url}
|
||||||
|
),
|
||||||
|
tags.input_({"type": "submit", "value": "Subscribe"}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -497,7 +497,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
feeds.sort(key=feed.sort_key, reverse=True)
|
feeds.sort(key=feed.sort_key, reverse=True)
|
||||||
|
|
||||||
document = views.feed_view(feeds)
|
document = views.feed_view(feeds)
|
||||||
self.write_html(document.render())
|
self.write_html("<!DOCTYPE html>\n" + document.render())
|
||||||
|
|
||||||
def serve_subscribe_choose(self):
|
def serve_subscribe_choose(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -512,7 +512,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
|
|
||||||
document = views.subscribe_choose_view(candidates)
|
document = views.subscribe_choose_view(candidates)
|
||||||
self.write_html(document.render())
|
self.write_html("<!DOCTYPE html>\n" + document.render())
|
||||||
|
|
||||||
def write_html(self, html: str):
|
def write_html(self, html: str):
|
||||||
response = html.encode("utf-8")
|
response = html.encode("utf-8")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue