Merge branch 'filter-by-feed-dialog'

This commit is contained in:
Ondřej Nývlt 2023-08-27 13:21:07 +02:00
commit ec03dace99
8 changed files with 288 additions and 119 deletions

View file

@ -8,7 +8,7 @@ import confuse
import re
DATABASE = "../data/diffs.db"
DATABASE = "file:../data/diffs.db?mode=ro"
CONFIG_FILE = "../data/config.yaml"
config = confuse.Configuration("headline", __name__)
@ -24,7 +24,7 @@ assets.url_expire = False
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db = g._database = sqlite3.connect(DATABASE, uri=True)
db.row_factory = sqlite3.Row
return db
@ -49,39 +49,86 @@ def websearch_to_fts_query(search: str):
)
def get_feeds():
return [
{
"rss_source": str(conf["rss_source"]),
"unique_tag": str(conf["unique_tag"]),
"feed_name": str(conf["name"]),
}
for conf in config["feeds"]
]
@app.route("/")
def index():
db = get_db().cursor()
search = request.args.get("search", type=str, default="")
query = websearch_to_fts_query(search) if search else None
selected_feeds = request.args.getlist("feeds[]")
db.execute(
f"SELECT count(*) FROM diffs{'_fts(?)' if query else ''}",
(query,) if query else (),
)
sql_select = "SELECT * FROM diffs "
sql_count = "SELECT count(*) FROM diffs "
diff_count = db.fetchall()[0][0]
if query:
sql_part_query = f"JOIN (SELECT rowid FROM diffs_fts({query})) filter ON filter.rowid = diffs.diff_id "
sql_select = sql_select + sql_part_query
sql_count = sql_count + sql_part_query
if selected_feeds:
feeds = str(selected_feeds).strip("[]")
sql_part_feeds = f"WHERE feed_name in ({feeds}) "
sql_select = sql_select + sql_part_feeds
sql_count = sql_count + sql_part_feeds
# flask-paginate
page = request.args.get(get_page_parameter(), type=int, default=1)
db.execute(sql_count)
diff_count = db.fetchall()[0][0]
pagination = Pagination(
page=page, total=diff_count, record_name="diffs", css_framework="bootstrap5"
)
page_skip = pagination.skip
per_page = pagination.per_page
if query:
db.execute(
"SELECT * FROM diffs JOIN (SELECT rowid FROM diffs_fts(?)) filter ON filter.rowid = diffs.diff_id ORDER BY diff_id DESC LIMIT ? OFFSET ?",
(query, per_page, page_skip),
)
else:
db.execute(
"SELECT * FROM diffs ORDER BY diff_id DESC LIMIT ? OFFSET ?",
(per_page, page_skip),
)
# Create and execute final query after getting page info
sql_part_pagination = f"ORDER BY diff_id DESC LIMIT {per_page} OFFSET {page_skip} "
sql_select = sql_select + sql_part_pagination
print(sql_select)
db.execute(sql_select)
# This would be a cleaner way to do it, but I have no clue how to make it work. Giving multiple feeds to the query is just seemingly impossible in sqlite.
# What about switching to Elasticsearch? :)
# if selected_feeds and query:
# feeds = str(selected_feeds).strip("[]")
# db.execute(
# "SELECT * FROM diffs JOIN (SELECT rowid FROM diffs_fts(?)) filter ON filter.rowid = diffs.diff_id WHERE feed_name IN (?) ORDER BY diff_id DESC LIMIT ? OFFSET ?",
# (query, feeds, per_page, page_skip),
# )
# elif query:
# db.execute(
# "SELECT * FROM diffs JOIN (SELECT rowid FROM diffs_fts(?)) filter ON filter.rowid = diffs.diff_id ORDER BY diff_id DESC LIMIT ? OFFSET ?",
# (query, per_page, page_skip),
# )
# elif selected_feeds:
# feeds = str(selected_feeds).strip("[]").replace("'", '"')
# print(feeds)
# db.execute(
# f"SELECT * FROM diffs WHERE feed_name IN ({','.join(['?']*len(selected_feeds))}) ORDER BY diff_id DESC LIMIT ? OFFSET ?",
# (selected_feeds, per_page, page_skip),
# )
# else:
# db.execute(
# "SELECT * FROM diffs ORDER BY diff_id DESC LIMIT ? OFFSET ?",
# (per_page, page_skip),
# )
diffs = db.fetchall()
html = render_template(
@ -91,6 +138,8 @@ def index():
pagination=pagination,
diff_count=diff_count,
search=search,
feeds=get_feeds(),
selected_feeds=selected_feeds,
)
res = make_response(html)
@ -125,15 +174,7 @@ def about():
@app.route("/feeds")
def feed_list():
feeds = []
for conf in config["feeds"]:
feed = {
"rss_source": str(conf["rss_source"]),
"unique_tag": str(conf["unique_tag"]),
"feed_name": str(conf["name"]),
}
feeds.append(feed)
return render_template("feeds.html", feeds=feeds)
return render_template("feeds.html", feeds=get_feeds())
@app.route("/robots.txt")

View file

@ -30,7 +30,7 @@
border-color: var(--color-border);
}
* {
:not(dialog) {
margin: 0;
}
@ -132,18 +132,36 @@
margin-right: 0.5rem;
}
.button {
.button,
.button-outline {
display: inline-flex;
align-items: center;
flex-shrink: 0;
gap: 0.5em;
border-radius: 0.25rem;
transition: background-color 120ms;
}
.button {
border: 1px solid var(--color-border);
background: #f3f4f6;
transition: background-color 120ms;
color: #4b5563;
}
.button:not(:disabled):hover {
background: hsl(0 0% 90%);
}
.button-outline {
border: 1px solid var(--color-border);
background: white;
color: #4b5563;
}
.button-outline:not(:disabled):hover {
background: hsl(0 0% 95%);
}
.button-md {
font-size: 0.9em;
line-height: 1.5rem;
@ -151,14 +169,10 @@
padding: 0.375rem 0.75rem;
}
.button:not(:disabled) {
button:not(:disabled) {
cursor: pointer;
}
.button:not(:disabled):hover {
background: hsl(0 0% 90%);
}
/* Pagination */
.pagination {
@ -234,19 +248,37 @@
border-bottom-width: 1px;
}
[hx-swap-oob] {
display: none;
}
.dialog {
border-radius: 0.5rem;
border: 1px solid var(--color-border);
box-shadow: var(--un-shadow-inset) 0 1px 3px 0 rgba(0,0,0,0.1),var(--un-shadow-inset) 0 1px 2px -1px rgba(0,0,0,0.1);
}
.dialog::backdrop {
background-color: hsl(0 0% 90% / 80%);
}
/* layer: shortcuts */
.container{padding-left:1rem;padding-right:1rem;margin-left:auto;margin-right:auto;max-width:1200px;}
.action-link{font-size:0.875rem;line-height:1.25rem;font-weight:500;--un-text-opacity:1;color:rgba(107,114,128,var(--un-text-opacity));display:inline-flex;gap:0.375rem;align-items:center;}
.text-caption{font-size:0.875rem;line-height:1.25rem;font-weight:500;--un-text-opacity:1;color:rgba(107,114,128,var(--un-text-opacity));}
.text-muted{--un-text-opacity:1;color:rgba(107,114,128,var(--un-text-opacity));}
.action-link:hover{--un-text-opacity:1;color:rgba(29,78,216,var(--un-text-opacity));}
/* layer: default */
.p-0{padding:0;}
.px-2{padding-left:0.5rem;padding-right:0.5rem;}
.px-4{padding-left:1rem;padding-right:1rem;}
.px-5{padding-left:1.25rem;padding-right:1.25rem;}
.py-1\.5{padding-top:0.375rem;padding-bottom:0.375rem;}
.py-3{padding-top:0.75rem;padding-bottom:0.75rem;}
.py-4{padding-top:1rem;padding-bottom:1rem;}
.py-6{padding-top:1.5rem;padding-bottom:1.5rem;}
.pr-2{padding-right:0.5rem;}
.mx-2{margin-left:0.5rem;margin-right:0.5rem;}
.my-6{margin-top:1.5rem;margin-bottom:1.5rem;}
.mb-2{margin-bottom:0.5rem;}
.mb-3{margin-bottom:0.75rem;}
@ -254,18 +286,24 @@
.mb-6{margin-bottom:1.5rem;}
.mb-8{margin-bottom:2rem;}
.mb-auto{margin-bottom:auto;}
.ml-2{margin-left:0.5rem;}
.mr-2{margin-right:0.5rem;}
.mt-12{margin-top:3rem;}
.mt-3{margin-top:0.75rem;}
.block{display:block;}
.display-none,
.hidden{display:none;}
.bg-white{--un-bg-opacity:1;background-color:rgba(255,255,255,var(--un-bg-opacity));}
.hover\:bg-gray-100:hover{--un-bg-opacity:1;background-color:rgba(243,244,246,var(--un-bg-opacity));}
.fill-current{fill:currentColor;}
.border-b{border-bottom-width:1px;}
.rounded{border-radius:0.25rem;}
.text-2xl{font-size:1.5rem;line-height:2rem;}
.text-4xl{font-size:2.25rem;line-height:2.5rem;}
.text-lg{font-size:1.125rem;line-height:1.75rem;}
.text-sm{font-size:0.875rem;line-height:1.25rem;}
.text-xl{font-size:1.25rem;line-height:1.75rem;}
.text-xs{font-size:0.75rem;line-height:1rem;}
.font-bold{font-weight:700;}
.font-medium{font-weight:500;}
.text-black{--un-text-opacity:1;color:rgba(0,0,0,var(--un-text-opacity));}
@ -278,22 +316,25 @@
.shrink-0{flex-shrink:0;}
.flex-wrap{flex-wrap:wrap;}
.gap-2{gap:0.5rem;}
.gap-3{gap:0.75rem;}
.gap-x-2{column-gap:0.5rem;}
.gap-x-4{column-gap:1rem;}
.gap-x-6{column-gap:1.5rem;}
.gap-x-8{column-gap:2rem;}
.gap-y-2{row-gap:0.5rem;}
.sticky{position:sticky;}
.static{position:static;}
.max-w-20rem{max-width:20rem;}
.max-w-800px{max-width:800px;}
.w-3{width:0.75rem;}
.w-4{width:1rem;}
.w-full{width:100%;}
.overflow-x-auto{overflow-x:auto;}
.justify-between{justify-content:space-between;}
.items-center{align-items:center;}
.top-0{top:0;}
.z-10{z-index:10;}
.transition-color{transition-property:color;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
.columns-2{columns:2;}
@media (min-width: 768px){
.md\:hidden{display:none;}
.md\:display-table{display:table;}

View file

@ -41,3 +41,4 @@
{% include "parts/footer.html" %}
</body>
</html>
{% block top %}{% endblock %}

View file

@ -4,84 +4,18 @@
{% block body %}
<div class="bg-white sticky top-0 py-3 border-b z-10 mb-6">
<section class="container flex items-center flex-wrap shrink-0 gap-x-6 gap-y-2">
<form method="get" class="flex gap-x-2">
<input
class="text-input w-full max-w-20rem"
type="text"
id="search"
name="search"
value="{{ search|e }}"
/>
<button
class="button button-md"
type="submit"
value="Hledat"
>
Hledat
<svg class="w-4 fill-current" 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>
</form>
{% include "parts/filters.html" %}
</div>
<label class="checkbox">
<input type="checkbox" x-model="expandDiffs" x-bind:checked="expandDiffs" />
<p>Expand diffs</p>
</label>
<div id="diff-list">
<section class="container mb-8">
{% include "parts/diff_list.html" %}
</section>
</div>
<section class="container mb-8">
{% for diff in diffs %}
<article class="card px-5 py-4 mb-4">
<p class="flex gap-x-4 gap-y-2 mb-3 flex-wrap shrink-0">
<span class="text-sm font-medium">{{ diff.feed_name }}</span>
<time class="text-sm font-medium">{{ diff.diff_time }}</time>
<a class="action-link" href="{{ diff.article_url }}">
<svg class="w-4 fill-current" 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="action-link" href="/article/{{ diff.article_id }}">
<svg class="w-4 fill-current" 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>
<div class="text-lg" x-bind:class="{ hidden: expandDiffs }">{{ diff.diff_html|safe }}</div>
<div x-bind:class="{ hidden: !expandDiffs }">
<div class="md:hidden">
<div class="mb-2">
<p class="text-caption">Before</p>
<p class="diff-before text-lg">{{ diff.diff_html|safe }}</p>
</div>
<div>
<p class="text-caption">After</p>
<p class="diff-after text-lg">{{ diff.diff_html|safe }}</p>
</div>
</div>
<table class="display-none md:display-table">
<tr>
<th class="text-caption pr-2">Before</th>
<td class="diff-before text-lg">{{ diff.diff_html|safe }}</td>
</tr>
<tr>
<th class="text-caption pr-2">After</th>
<td class="diff-after text-lg">{{ diff.diff_html|safe }}</td>
</tr>
</table>
</div>
</article>
{% endfor %}
</section>
<div class="overflow-x-auto">
<div class="container mb-3">
{{ pagination.links }}
<div class="overflow-x-auto">
<div class="container mb-3">{{ pagination.links }}</div>
</div>
</div>
<div class="container text-sm text-gray-500">
{{ pagination.info }}
<div class="container text-sm text-gray-500">{{ pagination.info }}</div>
</div>
{% endblock body %}

View file

@ -0,0 +1,51 @@
{% for diff in diffs %}
<article class="card px-5 py-4 mb-4">
<p class="flex gap-x-4 gap-y-2 mb-3 flex-wrap shrink-0">
<span class="text-sm font-medium">{{ diff.feed_name }}</span>
<time class="text-sm font-medium">{{ diff.diff_time }}</time>
<a class="action-link" href="{{ diff.article_url }}">
<svg class="w-4 fill-current" 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="action-link" href="/article/{{ diff.article_id }}">
<svg class="w-4 fill-current" 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>
<div class="text-lg" x-bind:class="{ hidden: expandDiffs }">
{{ diff.diff_html|safe }}
</div>
<div x-bind:class="{ hidden: !expandDiffs }">
<div class="md:hidden">
<div class="mb-2">
<p class="text-caption">Before</p>
<p class="diff-before text-lg">{{ diff.diff_html|safe }}</p>
</div>
<div>
<p class="text-caption">After</p>
<p class="diff-after text-lg">{{ diff.diff_html|safe }}</p>
</div>
</div>
<table class="display-none md:display-table">
<tr>
<th class="text-caption pr-2">Before</th>
<td class="diff-before text-lg">{{ diff.diff_html|safe }}</td>
</tr>
<tr>
<th class="text-caption pr-2">After</th>
<td class="diff-after text-lg">{{ diff.diff_html|safe }}</td>
</tr>
</table>
</div>
</article>
{% endfor %}

View file

@ -0,0 +1,24 @@
<dialog hx-boost="false" class="dialog" x-ref="feedsDialog">
<header class="flex justify-between mb-6 items-center">
<p>Filter by feed</p>
<button formmethod="dialog" class="button-outline button-md" @click="$refs.feedsDialog.close()">
</button>
</header>
<div class="columns-2 mb-6">
{% for feed in feeds %}
<label
class="flex items-center gap-3 px-2 py-1.5 hover:bg-gray-100 rounded"
>
<input type="checkbox" name="feeds[]" value="{{ feed['feed_name'] }}"
{{ 'checked' if feed['feed_name'] in selected_feeds }} > {{
feed['feed_name'] }}
</label>
{% endfor %}
</div>
<button type="submit" class="button button-md">
Filter diffs
</button>
</dialog>

View file

@ -0,0 +1,48 @@
<section class="container">
<div class="flex items-center flex-wrap shrink-0 gap-x-2 gap-y-2">
<form method="get" action="/" class="flex gap-x-2">
<input
class="text-input w-full max-w-20rem"
type="search"
id="search"
name="search"
value="{{ search|e }}"
/>
<button class="button button-md" type="submit" value="Hledat">
Search
<svg class="w-4 fill-current" 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>
<button type="button" class="button-outline button-md" @click="$refs.feedsDialog.showModal()">
<svg class="w-4 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 14L4 5V3H20V5L14 14V20L10 22V14Z"></path></svg>
Feeds
</button>
{% include "parts/feeds_filter_dialog.html" %}
</form>
<label class="checkbox mx-2">
<input
type="checkbox"
x-model="expandDiffs"
x-bind:checked="expandDiffs"
/>
<p>Expand diffs</p>
</label>
</div>
{% if selected_feeds|length > 0 %}
<div class="text-xs font-medium text-muted flex items-center mt-3">
<svg class="w-3 fill-current mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 14L4 5V3H20V5L14 14V20L10 22V14Z"></path></svg>
<div>
Feeds: <span>{{ ", ".join(selected_feeds) }}</span>
<a href="/" class="ml-2">✕ Clear</a>
</div>
</div>
{% endif %}
</section>

View file

@ -31,7 +31,7 @@ const globalCss = (theme: Required<Theme>) => css`
border-color: var(--color-border);
}
* {
:not(dialog) {
margin: 0;
}
@ -133,18 +133,36 @@ const globalCss = (theme: Required<Theme>) => css`
margin-right: 0.5rem;
}
.button {
.button,
.button-outline {
display: inline-flex;
align-items: center;
flex-shrink: 0;
gap: 0.5em;
border-radius: ${theme.borderRadius.sm};
transition: background-color 120ms;
}
.button {
border: 1px solid var(--color-border);
background: ${theme.colors.gray[100]};
transition: background-color 120ms;
color: ${theme.colors.gray[600]};
}
.button:not(:disabled):hover {
background: hsl(0 0% 90%);
}
.button-outline {
border: 1px solid var(--color-border);
background: white;
color: ${theme.colors.gray[600]};
}
.button-outline:not(:disabled):hover {
background: hsl(0 0% 95%);
}
.button-md {
font-size: 0.9em;
line-height: 1.5rem;
@ -152,14 +170,10 @@ const globalCss = (theme: Required<Theme>) => css`
padding: 0.375rem 0.75rem;
}
.button:not(:disabled) {
button:not(:disabled) {
cursor: pointer;
}
.button:not(:disabled):hover {
background: hsl(0 0% 90%);
}
/* Pagination */
.pagination {
@ -234,6 +248,20 @@ const globalCss = (theme: Required<Theme>) => css`
.table-styled thead tr {
border-bottom-width: 1px;
}
[hx-swap-oob] {
display: none;
}
.dialog {
border-radius: ${theme.borderRadius["md"]};
border: 1px solid var(--color-border);
box-shadow: ${theme.boxShadow.DEFAULT};
}
.dialog::backdrop {
background-color: hsl(0 0% 90% / 80%);
}
`;
/**
@ -261,6 +289,7 @@ export default defineConfig({
md: "0.5rem",
},
},
rules: [[/^columns-(\d+)$/, ([_, num]) => ({ columns: num })]],
shortcuts: {
container: "max-w-1200px px-4 mx-auto",
"action-link":