diff --git a/view/app.py b/view/app.py index 47d5685..4f493db 100644 --- a/view/app.py +++ b/view/app.py @@ -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") diff --git a/view/static/main.css b/view/static/main.css index c4a2561..571fd11 100644 --- a/view/static/main.css +++ b/view/static/main.css @@ -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;} diff --git a/view/templates/base.html b/view/templates/base.html index e0259f0..169ac96 100644 --- a/view/templates/base.html +++ b/view/templates/base.html @@ -41,3 +41,4 @@ {% include "parts/footer.html" %} +{% block top %}{% endblock %} diff --git a/view/templates/index.html b/view/templates/index.html index df09e21..dfbd4ab 100644 --- a/view/templates/index.html +++ b/view/templates/index.html @@ -4,84 +4,18 @@ {% block body %}
-
-
- - -
+ {% include "parts/filters.html" %} +
- +
+
+ {% include "parts/diff_list.html" %}
-
-
- {% for diff in diffs %} -
-

- {{ diff.feed_name }} - - - - Display current article - - - - Show change history - -

- -
{{ diff.diff_html|safe }}
-
-
-
-

Before

-

{{ diff.diff_html|safe }}

-
-
-

After

-

{{ diff.diff_html|safe }}

-
-
- - - - - - - - - - - -
-
- {% endfor %} -
- -
-
- {{ pagination.links }} +
+
{{ pagination.links }}
-
-
- {{ pagination.info }} + +
{{ pagination.info }}
{% endblock body %} diff --git a/view/templates/parts/diff_list.html b/view/templates/parts/diff_list.html new file mode 100644 index 0000000..c579850 --- /dev/null +++ b/view/templates/parts/diff_list.html @@ -0,0 +1,51 @@ +{% for diff in diffs %} +
+

+ {{ diff.feed_name }} + + + + + + Display current article + + + + + + Show change history + +

+ +
+ {{ diff.diff_html|safe }} +
+
+
+
+

Before

+

{{ diff.diff_html|safe }}

+
+
+

After

+

{{ diff.diff_html|safe }}

+
+
+ + + + + + + + + + + +
+
+ {% endfor %} diff --git a/view/templates/parts/feeds_filter_dialog.html b/view/templates/parts/feeds_filter_dialog.html new file mode 100644 index 0000000..5371c46 --- /dev/null +++ b/view/templates/parts/feeds_filter_dialog.html @@ -0,0 +1,24 @@ + +
+

Filter by feed

+ +
+ +
+ {% for feed in feeds %} + + {% endfor %} +
+ + +
diff --git a/view/templates/parts/filters.html b/view/templates/parts/filters.html new file mode 100644 index 0000000..a22f86c --- /dev/null +++ b/view/templates/parts/filters.html @@ -0,0 +1,48 @@ +
+
+
+ + + + + + + {% include "parts/feeds_filter_dialog.html" %} +
+ + +
+ + {% if selected_feeds|length > 0 %} +
+ +
+ Feeds: {{ ", ".join(selected_feeds) }} + ✕ Clear +
+
+ {% endif %} +
diff --git a/view/uno.config.ts b/view/uno.config.ts index 4d6b2b0..7b471c9 100644 --- a/view/uno.config.ts +++ b/view/uno.config.ts @@ -31,7 +31,7 @@ const globalCss = (theme: Required) => css` border-color: var(--color-border); } - * { + :not(dialog) { margin: 0; } @@ -133,18 +133,36 @@ const globalCss = (theme: Required) => 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) => 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) => 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":