mirror of
https://git.nolog.cz/NoLog.cz/headline.git
synced 2025-01-31 11:53:35 +01:00
Merge pull request 'New frontent and article grouping' (#1) from new-frontend into main
Reviewed-on: https://git.nolog.cz/mdivecky/headline/pulls/1
This commit is contained in:
commit
b46af16644
15 changed files with 696 additions and 166 deletions
15
.editorconfig
Normal file
15
.editorconfig
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{html,css}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.html": "jinja-html"
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ services:
|
||||||
build: ./view/
|
build: ./view/
|
||||||
command: python app.py
|
command: python app.py
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5050:5000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./view:/app
|
- ./view:/app
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
|
44
misc/article_id_generator.py
Normal file
44
misc/article_id_generator.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
#
|
||||||
|
# Create a UID of the article in old articles where we don't have RSS UID and where we can't generate the article_id on the fly.
|
||||||
|
# It takes a while, but it's a one-shot.
|
||||||
|
#
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
db_con = sqlite3.connect("../data/diffs.db")
|
||||||
|
db = db_con.cursor()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_article_id(uid, feed):
|
||||||
|
# Create a fake unique ID from RSS unique tag and feed name to reference the article in database
|
||||||
|
id_string = str(uid) + str(feed)
|
||||||
|
id_bytes = id_string.encode('utf-8')
|
||||||
|
article_id = hashlib.sha256(id_bytes).hexdigest()
|
||||||
|
return(article_id)
|
||||||
|
|
||||||
|
|
||||||
|
def update_diff(diff_id, article_id):
|
||||||
|
sql = "UPDATE diffs SET article_id = ? WHERE diff_id = ?"
|
||||||
|
sql_data = (article_id, diff_id)
|
||||||
|
db.execute(sql, sql_data)
|
||||||
|
db_con.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
"SELECT * FROM diffs WHERE NOT 'article_id' ORDER BY diff_id DESC ",
|
||||||
|
)
|
||||||
|
diffs = db.fetchall()
|
||||||
|
|
||||||
|
for diff in diffs:
|
||||||
|
article_id = create_article_id(diff[1], diff[2])
|
||||||
|
update_diff(diff[0], article_id)
|
||||||
|
print(article_id)
|
|
@ -6,6 +6,7 @@ import redis
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from diff_match_patch import diff_match_patch
|
from diff_match_patch import diff_match_patch
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ db = db_con.cursor()
|
||||||
db.executescript("""
|
db.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS diffs (
|
CREATE TABLE IF NOT EXISTS diffs (
|
||||||
diff_id INTEGER PRIMARY KEY,
|
diff_id INTEGER PRIMARY KEY,
|
||||||
|
article_id TEXT,
|
||||||
feed_name TEXT NOT NULL,
|
feed_name TEXT NOT NULL,
|
||||||
article_url TEXT NOT NULL,
|
article_url TEXT NOT NULL,
|
||||||
title_orig TEXT NOT NULL,
|
title_orig TEXT NOT NULL,
|
||||||
|
@ -84,8 +86,8 @@ def process_diff(old, new, rss_id):
|
||||||
# print(old['link'])
|
# print(old['link'])
|
||||||
# print(diff)
|
# print(diff)
|
||||||
|
|
||||||
sql = "INSERT INTO diffs(feed_name, article_url, title_orig, title_new, diff_html, diff_time) VALUES (?,?,?,?,?,datetime('now', 'localtime'))"
|
sql = "INSERT INTO diffs(article_id, feed_name, article_url, title_orig, title_new, diff_html, diff_time) VALUES (?,?,?,?,?,datetime('now', 'localtime'))"
|
||||||
sql_data = (old['medium'], old['link'], old['title'], new['title'], html_diff)
|
sql_data = (new['article_id'], old['medium'], old['link'], old['title'], new['title'], html_diff)
|
||||||
db.execute(sql, sql_data)
|
db.execute(sql, sql_data)
|
||||||
db_con.commit()
|
db_con.commit()
|
||||||
|
|
||||||
|
@ -108,7 +110,12 @@ def process_item(article, rc):
|
||||||
# Article is new, just create it and exit
|
# Article is new, just create it and exit
|
||||||
write_article(article, rc)
|
write_article(article, rc)
|
||||||
|
|
||||||
|
def create_article_id(uid, feed):
|
||||||
|
# Create a unique ID from RSS unique tag and feed name to reference the article in database
|
||||||
|
id_string = str(uid) + str(feed)
|
||||||
|
id_bytes = id_string.encode('utf-8')
|
||||||
|
article_id = hashlib.sha256(id_bytes).hexdigest()
|
||||||
|
return(article_id)
|
||||||
|
|
||||||
|
|
||||||
for feed in config['feeds']:
|
for feed in config['feeds']:
|
||||||
|
@ -123,11 +130,13 @@ for feed in config['feeds']:
|
||||||
try:
|
try:
|
||||||
rss_id = item[unique_tag]
|
rss_id = item[unique_tag]
|
||||||
title = item['title']
|
title = item['title']
|
||||||
|
article_id = create_article_id(rss_id, name)
|
||||||
#description = item['description'] ## Don't store description for now, as we don't need it and it's big.
|
#description = item['description'] ## Don't store description for now, as we don't need it and it's big.
|
||||||
published = time.strftime('%Y:%m:%d %H:%M:%S %Z %z', item['published_parsed'])
|
published = time.strftime('%Y:%m:%d %H:%M:%S %Z %z', item['published_parsed'])
|
||||||
link = item['link']
|
link = item['link']
|
||||||
article_data = {
|
article_data = {
|
||||||
'title' : title,
|
'title' : title,
|
||||||
|
'article_id': article_id,
|
||||||
#'description': description,
|
#'description': description,
|
||||||
'published' : published,
|
'published' : published,
|
||||||
'link' : link,
|
'link' : link,
|
||||||
|
|
10
view/app.py
10
view/app.py
|
@ -80,6 +80,16 @@ def index():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/article/<path:article_id>")
|
||||||
|
def article_detail(article_id: str):
|
||||||
|
db = get_db().cursor()
|
||||||
|
db.execute("SELECT * FROM diffs WHERE article_id = ?", (article_id,))
|
||||||
|
result = db.fetchall()
|
||||||
|
article_url = result[0]['article_url']
|
||||||
|
# TODO: Handle if nothing is found and return 404 in that case.
|
||||||
|
return render_template("article_detail.html", article_id=article_id, article_url=article_url, diffs=result )
|
||||||
|
|
||||||
|
|
||||||
@app.route('/about')
|
@app.route('/about')
|
||||||
def about():
|
def about():
|
||||||
return render_template('about.html')
|
return render_template('about.html')
|
||||||
|
|
|
@ -0,0 +1,372 @@
|
||||||
|
/* Global */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--border-color: hsl(0 0% 80% / 60%);
|
||||||
|
--accent-color: hsl(225 90% 50%);
|
||||||
|
--accent-color-pressed: hsl(225 90% 35%);
|
||||||
|
--color-muted: hsl(0 0% 50%);
|
||||||
|
--radius-s: 0.25em;
|
||||||
|
--radius-m: 0.5em;
|
||||||
|
--font-size-m: 1rem;
|
||||||
|
--font-size-s: 0.85rem;
|
||||||
|
--font-size-xs: 0.75rem;
|
||||||
|
--font-size-l: 1.25rem;
|
||||||
|
--box-shadow: 0 2px 0 hsl(0 0% 50% / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
line-height: 1.5;
|
||||||
|
background-color: hsl(0 0% 98%);
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
picture,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
th,
|
||||||
|
tr,
|
||||||
|
td {
|
||||||
|
text-align: inherit;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--accent-color-pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
ins {
|
||||||
|
background-color: hsl(120 100% 95%);
|
||||||
|
text-decoration-color: hsl(120 50% 75% / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
del {
|
||||||
|
background-color: hsl(0 100% 95%);
|
||||||
|
text-decoration-color: hsl(0 50% 40% / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary::before {
|
||||||
|
content: url('data:image/svg+xml,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.1714 12.0007L8.22168 7.05093L9.63589 5.63672L15.9999 12.0007L9.63589 18.3646L8.22168 16.9504L13.1714 12.0007Z"></path></svg>');
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
transition: transform 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header:not(.header-extended) {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link-home {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 .del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 .ins {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-decoration-style: wavy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 4rem;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
color: hsl(0 0% 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nologo {
|
||||||
|
display: inline-block;
|
||||||
|
fill: hsl(0 0% 60%);
|
||||||
|
transition: fill 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nologo:hover {
|
||||||
|
fill: hsl(0 0% 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
transition: border-color 120ms, box-shadow 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px hsl(225 90% 40% / 50%);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
background: hsl(0 0% 95%);
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
transition: background-color 120ms;
|
||||||
|
color: hsl(0 0% 40%);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: hsl(0 0% 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 2px 0 hsl(0 0% 50% / 20%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: hsl(0 0% 95%);
|
||||||
|
transition: background-color 120ms;
|
||||||
|
color: hsl(0 0% 40%);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:not(.active):hover {
|
||||||
|
background: hsl(0 0% 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:first-of-type {
|
||||||
|
border-radius: var(--radius-s) 0 0 var(--radius-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:last-of-type {
|
||||||
|
border-radius: 0 var(--radius-s) var(--radius-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:not(:last-of-type) {
|
||||||
|
border-right-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-page-info {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose p:not(:last-of-type) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Index */
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: white;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changesets {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset-feed-name,
|
||||||
|
.changeset-time,
|
||||||
|
.changeset-action {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset-action:first-of-type {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset-title {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-icon {
|
||||||
|
display: inline-block;
|
||||||
|
fill: currentColor;
|
||||||
|
width: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset details[open] summary {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeset table th {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
|
@ -1,42 +1,32 @@
|
||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en" class="h100">
|
|
||||||
|
|
||||||
<head>
|
{% block head %}
|
||||||
{% include 'parts/head.html' %}
|
<style>
|
||||||
<style>
|
.prose {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock head %}
|
||||||
|
|
||||||
html {
|
{% block body %}
|
||||||
position: relative;
|
<div class="container">
|
||||||
min-height: 100%;
|
<div class="prose">
|
||||||
}
|
<p>
|
||||||
|
Headliner is monitoring rss feeds of czech news websites for changes in
|
||||||
|
article headlines. Just because it might be interesting.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Check out <a href="https://git.nolog.cz/mdivecky/headline">the source code</a>,
|
||||||
|
but be aware that it's not too nice. Feel free to improve it or run the tool yourself.
|
||||||
|
</p>
|
||||||
|
|
||||||
body {
|
<p>
|
||||||
margin-bottom: 60px; /* Margin bottom by footer height */
|
If you want to access the raw data collected by this tool, you can
|
||||||
}
|
<a href="https://git.nolog.cz/NoLog.cz/headline-exports"> download the full archive from our git repo.</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
.footer {
|
<p><a href="https://ondrej.nyv.lt/">Ondřej</a> and <a href="https://bain.cz">Bain</a> made important contribution to the project. Thank you!</p>
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="d-flex flex-column h-100">
|
|
||||||
|
|
||||||
<!-- Begin page content -->
|
|
||||||
<main class="flex-shrink-0">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="mt-5">Headliner</h1>
|
|
||||||
<p class="lead">Headliner is monitoring rss feeds of czech news websites for changes in article headlines. Just
|
|
||||||
because it might be interesting.</p>
|
|
||||||
<p>See <a href="https://git.nolog.cz/mdivecky/headline">the source code</a>, but be aware that it's not too nice.
|
|
||||||
Feel free to improve it.</p>
|
|
||||||
<p>If you want to access the raw data collected by this tool, you can <a href="https://git.nolog.cz/NoLog.cz/headline-exports">download the full archive from our git</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
{% endblock body %}
|
||||||
{% include 'parts/footer.html' %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
47
view/templates/article_detail.html
Normal file
47
view/templates/article_detail.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
.page-heading {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffs-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-table th, .diff-table td {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-table th {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-table tr:not(:last-of-type) {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock head %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>Diffs for the article at <a href="{{ article_url }}">{{ article_url|truncate(50) }}</a></h1>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<table class="diff-table">
|
||||||
|
{% for diff in diffs %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ diff.diff_time }}</th>
|
||||||
|
<td>{{ diff.diff_html|safe }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock body %}
|
41
view/templates/base.html
Normal file
41
view/templates/base.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Headliner</title>
|
||||||
|
<link
|
||||||
|
rel="shortcut icon"
|
||||||
|
href="{{ url_for('static', filename='favicon.ico') }}"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='main.css') }}"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
data-domain="headline.beta.nolog.cz"
|
||||||
|
src="https://plausible.nolog.cz/js/plausible.js"
|
||||||
|
></script>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header {% block header_class %}{% endblock %}">
|
||||||
|
<div class="container">
|
||||||
|
<a class="header-link-home" href="/">
|
||||||
|
<h1>
|
||||||
|
<span class="del"> Head</span><span class="ins">liner</span>
|
||||||
|
</h1>
|
||||||
|
</a>
|
||||||
|
<nav>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
<a href="/feeds">Feeds</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="main">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
{% include "parts/footer.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,13 +1,7 @@
|
||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
{% block body %}
|
||||||
{% include 'parts/head.html' %}
|
<div class="container">
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -25,11 +19,9 @@
|
||||||
<td>{{ feed.unique_tag }}</td>
|
<td>{{ feed.unique_tag }}</td>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'parts/footer.html' %}
|
{% endblock body %}
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -1,80 +1,68 @@
|
||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
{% block header_class %}header-extended{% endblock header_class %}
|
||||||
{% include 'parts/head.html' %}
|
|
||||||
|
|
||||||
<style>
|
{% block body %}
|
||||||
/* Longer name hidden by default */
|
<div class="filters">
|
||||||
span.long {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* On hover, hide the short name */
|
|
||||||
.expanded:hover span.short {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* On hover, display the longer name. */
|
|
||||||
.expanded:hover span.long {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<form>
|
<form method="get">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<input class="m-2 form-control" type="text" id="search" name="search" value="{{ search|e }}" />
|
<input
|
||||||
<input class="m-2 btn btn-primary" type="submit" formenctype="application/x-www-form-urlencoded" formmethod="get" value="Hledat" />
|
class="input"
|
||||||
|
type="text"
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
|
value="{{ search|e }}"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
type="submit"
|
||||||
|
value="Hledat"
|
||||||
|
>
|
||||||
|
Hledat
|
||||||
|
<svg class="inline-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Detection time</th>
|
|
||||||
<th>Source</th>
|
|
||||||
<th>Diff</th>
|
|
||||||
<th>Original</th>
|
|
||||||
<th>Changed</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for diff in diffs %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ diff.diff_time }}</td>
|
|
||||||
<td><a href='{{ diff.article_url }}' target="_blank">{{ diff.feed_name }}</a></td>
|
|
||||||
<td>{{ diff.diff_html|safe }}</td>
|
|
||||||
<td class="expanded">
|
|
||||||
<span class="short">{{ diff.title_orig|truncate(15) }} </span>
|
|
||||||
<span class="long">{{ diff.title_orig }} </span>
|
|
||||||
</td>
|
|
||||||
<td class="expanded">
|
|
||||||
<span class="short">{{ diff.title_new|truncate(15) }} </span>
|
|
||||||
<span class="long">{{ diff.title_new}} </span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="container text-center">
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
{{ pagination.links }}
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
{{ pagination.info }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% include 'parts/footer.html' %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
<div class="container changesets">
|
||||||
|
{% for diff in diffs %}
|
||||||
|
<article class="card changeset">
|
||||||
|
<p class="changeset-actions">
|
||||||
|
<span class="changeset-feed-name">{{ diff.feed_name }}</span>
|
||||||
|
<time class="changeset-time">{{ diff.diff_time }}</time>
|
||||||
|
<a class="changeset-action" href="{{ diff.article_url }}">
|
||||||
|
<svg class="inline-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"></path></svg>
|
||||||
|
Display current article
|
||||||
|
</a>
|
||||||
|
<a class="changeset-action" href="/article/{{ diff.article_id }}">
|
||||||
|
<svg class="inline-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path></svg>
|
||||||
|
Show change history
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<details>
|
||||||
|
<summary class="changeset-title">
|
||||||
|
<h2 class="diff">{{ diff.diff_html|safe }}</h2>
|
||||||
|
</summary>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Old</th>
|
||||||
|
<td>{{ diff.title_orig }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>New</th>
|
||||||
|
<td>{{ diff.title_new }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{{ pagination.links }}
|
||||||
|
{{ pagination.info }}
|
||||||
|
</div>
|
||||||
|
{% endblock body %}
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
<footer class="footer mt-auto py-3 bg-light">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container footer-container">
|
||||||
<span class="text-muted"> <a href="{{ url_for('index') }}">Headliner</a> | <a href="{{ url_for('feed_list') }}">Feed list</a> | <a href="{{ url_for('about') }}">About</a> | <a href="https://nolog.cz">NoLog.cz</a></span>
|
<span>created by</span>
|
||||||
|
<a href="https://nolog.cz">
|
||||||
|
<svg class="footer-nologo" width="120" viewBox="0 0 400 60" fill-rule="evenodd">
|
||||||
|
<title>NoLog</title>
|
||||||
|
<path d="M60,0l-60,0l0,60l20,0l0,-41l20,0l-0,41l20,0l-0,-60Z"></path>
|
||||||
|
<path d="M140,60l50,0l-0,-19l-30,0l0,-41l-20,0l0,60Z"></path>
|
||||||
|
<path d="M130,0l-60,0l0,60l60,0l0,-60Zm-40,19l0,22l20,0l0,-22l-20,0Z"></path>
|
||||||
|
<path d="M260,0l-60,0l-0,60l60,0l-0,-60Zm-40,19l-0,22l20,0l-0,-22l-20,0Z"></path>
|
||||||
|
<path d="M270,60l60,-0l-0,-35l-20,0l-0,16l-20,0l-0,-22l40,-0l-0,-19l-60,-0l-0,60Z"></path>
|
||||||
|
<path d="M372.164,2.865l6.571,20.729l21.265,0l-17.203,12.812l6.571,20.729l-17.204,-12.811l-17.203,12.811l6.571,-20.729l-17.204,-12.812l21.265,0l6.571,-20.729Z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
|
@ -1,7 +0,0 @@
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Headliner</title>
|
|
||||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
|
|
||||||
<script defer data-domain="headline.beta.nolog.cz" src="https://plausible.nolog.cz/js/plausible.js"></script>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
|
13
view/templates/parts/header.html
Normal file
13
view/templates/parts/header.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<a href="/">
|
||||||
|
<h1>
|
||||||
|
<span class="del"> Head</span><span class="ins">liner</span>
|
||||||
|
</h1>
|
||||||
|
</a>
|
||||||
|
<nav>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
<a href="/feeds">Feeds</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
Loading…
Reference in a new issue