-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
557 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,6 +77,8 @@ app = [ | |
"humanize>=4,!=4.7.*", | ||
# for config | ||
"PyYAML", | ||
# for v2 | ||
"WTForms", | ||
] | ||
|
||
# UNSTABLE PLUGINS | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from functools import partial | ||
|
||
from flask import Blueprint | ||
from flask import request | ||
|
||
from .. import get_reader | ||
from .. import stream_template | ||
from .forms import ENTRY_FILTER_PRESETS | ||
from .forms import EntryFilter | ||
|
||
|
||
blueprint = Blueprint( | ||
'v2', __name__, template_folder='templates', static_folder='static' | ||
) | ||
|
||
|
||
@blueprint.route('/') | ||
def entries(): | ||
reader = get_reader() | ||
|
||
# TODO: search | ||
# TODO: if search/tags is active, search/tags box should not be hidden | ||
# TODO: highlight active filter preset + uncollapse more | ||
# TODO: feed filter | ||
# TODO: pagination | ||
# TODO: read time | ||
# TODO: mark as ... | ||
# TODO: enclosures | ||
|
||
form = EntryFilter(request.args) | ||
kwargs = dict(form.data) | ||
del kwargs['search'] | ||
|
||
get_entries = reader.get_entries | ||
|
||
if form.validate(): | ||
entries = get_entries(**kwargs, limit=64) | ||
else: | ||
entries = [] | ||
|
||
return stream_template( | ||
'v2/entries.html', presets=ENTRY_FILTER_PRESETS, form=form, entries=entries | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import yaml | ||
from wtforms import Form | ||
from wtforms import RadioField | ||
from wtforms import SearchField | ||
from wtforms import StringField | ||
|
||
from reader._types import tag_filter_argument | ||
|
||
|
||
class TagFilterField(StringField): | ||
|
||
def process_formdata(self, valuelist): | ||
if not valuelist: | ||
return | ||
value = valuelist[0] | ||
if '[' not in value: | ||
value = f'[{value}]' | ||
try: | ||
data = yaml.safe_load(value) | ||
except yaml.error.MarkedYAMLError as e: | ||
raise ValueError(f"invalid YAML: {e.problem or e.context}") from e | ||
tag_filter_argument(data) | ||
self.data = data | ||
|
||
def _value(self): | ||
if self.raw_data: | ||
return self.raw_data[0] | ||
if not self.data: | ||
return '' | ||
return yaml.safe_dump(self.data, default_flow_style=True).rstrip() | ||
|
||
|
||
class ToFormdataMixin: | ||
def to_formdata(self): | ||
rv = {} | ||
|
||
for field in self: | ||
try: | ||
value = field._value() | ||
except AttributeError: | ||
values = [option._value() for option in field if option.checked] | ||
if values: | ||
value, *rest = values | ||
if rest: | ||
raise NotImplementedError( | ||
"multiple choices not supported" | ||
) from None | ||
else: | ||
value = field.default | ||
|
||
if value and value != field.default: | ||
rv[field.name] = value | ||
|
||
return rv | ||
|
||
|
||
def radio_field(*args, choices, **kwargs): | ||
"""Like RadioField, but choices is a list of (value, value_str), | ||
(value, value_str, label), or (value, value_str, label, render_kw) tuples. | ||
""" | ||
return RadioField( | ||
*args, | ||
choices=[c[1] if len(c) == 2 else c[1:] for c in choices], | ||
coerce={c[1]: c[0] for c in choices}.get, | ||
**kwargs, | ||
) | ||
|
||
|
||
BOOL_CHOICES = [(True, 'yes'), (False, 'no'), (None, 'all')] | ||
TRISTATE_CHOICES = [('notfalse', 'maybe')] + BOOL_CHOICES | ||
ENTRY_SORT_CHOICES = ['recent', 'random'] | ||
|
||
|
||
class EntryFilter(ToFormdataMixin, Form): | ||
search = SearchField("search", name='q') | ||
feed_tags = TagFilterField("tags", name='tags') | ||
read = radio_field("read", choices=BOOL_CHOICES, default='no') | ||
important = radio_field("important", choices=TRISTATE_CHOICES, default='maybe') | ||
has_enclosures = radio_field( | ||
"enclosures", name='enclosures', choices=BOOL_CHOICES, default='all' | ||
) | ||
sort = RadioField("sort", choices=ENTRY_SORT_CHOICES, default='recent') | ||
|
||
|
||
class SearchEntryFilter(EntryFilter): | ||
sort = RadioField( | ||
"sort", choices=ENTRY_SORT_CHOICES + ['relevant'], default='relevant' | ||
) | ||
|
||
|
||
ENTRY_FILTER_PRESETS = { | ||
'unread': {}, | ||
'important': {'read': 'all', 'important': 'yes'}, | ||
'podcast': {'enclosures': 'yes'}, | ||
'random': {'sort': 'random'}, | ||
} | ||
|
||
|
||
if __name__ == '__main__': | ||
from werkzeug.datastructures import MultiDict | ||
|
||
args = MultiDict(dict(tags='1')) | ||
for FormCls in EntryFilter, SearchEntryFilter: | ||
form = FormCls(args) | ||
for field in form: | ||
print(field()) | ||
print() | ||
print(form.data) | ||
print(form.to_formdata()) | ||
print() | ||
|
||
print(form.feed_tags.__dict__) | ||
import IPython | ||
|
||
IPython.embed() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
|
||
/* knock back heading sizes by 2 steps */ | ||
.h1, h1 { font-size: calc(1.3rem + .6vw); } | ||
.h2, h2 { font-size: calc(1.275rem + .3vw); } | ||
/* TODO: rest of them */ | ||
@media (min-width: 1200px) { | ||
.h1, h1 { font-size: 1.75rem; } | ||
.h2, h2 { font-size: 1.5rem; } | ||
/* TODO: rest of them */ | ||
} | ||
|
||
.nav.controls { | ||
--bs-nav-link-padding-x: 0; | ||
--bs-nav-link-padding-y: 0; | ||
gap: 1rem; | ||
} | ||
.nav.controls .nav-link.active { | ||
color: var(--bs-navbar-active-color); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/*! | ||
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) | ||
* Copyright 2011-2024 The Bootstrap Authors | ||
* Licensed under the Creative Commons Attribution 3.0 Unported License. | ||
*/ | ||
|
||
/* | ||
* Modified to use the Bootstrap Icons font, instead of SVG sprites. | ||
*/ | ||
|
||
(() => { | ||
'use strict' | ||
|
||
const getStoredTheme = () => localStorage.getItem('theme') | ||
const setStoredTheme = theme => localStorage.setItem('theme', theme) | ||
|
||
const getPreferredTheme = () => { | ||
const storedTheme = getStoredTheme() | ||
if (storedTheme) { | ||
return storedTheme | ||
} | ||
|
||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' | ||
} | ||
|
||
const setTheme = theme => { | ||
if (theme === 'auto') { | ||
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')) | ||
} else { | ||
document.documentElement.setAttribute('data-bs-theme', theme) | ||
} | ||
} | ||
|
||
const getIconCls = btn => { | ||
return btn.querySelector('.bi').classList.values().find(x => x.startsWith('bi-')) | ||
} | ||
|
||
setTheme(getPreferredTheme()) | ||
|
||
const showActiveTheme = (theme, focus = false) => { | ||
const themeSwitcher = document.querySelector('#theme') | ||
|
||
if (!themeSwitcher) { | ||
return | ||
} | ||
|
||
const themeSwitcherText = document.querySelector('#theme-text') | ||
const activeThemeIcon = document.querySelector('.theme-icon-active') | ||
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) | ||
const clsOfActiveBtn = btnToActive.querySelector('.bi').classList.values().find(x => x.startsWith('bi-')) | ||
|
||
document.querySelectorAll('[data-bs-theme-value]').forEach(element => { | ||
element.classList.remove('active') | ||
element.setAttribute('aria-pressed', 'false') | ||
}) | ||
|
||
btnToActive.classList.add('active') | ||
btnToActive.setAttribute('aria-pressed', 'true') | ||
activeThemeIcon.classList.remove( | ||
activeThemeIcon.classList.values().find(x => x.startsWith('bi-')) | ||
) | ||
activeThemeIcon.classList.add(clsOfActiveBtn) | ||
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})` | ||
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) | ||
|
||
if (focus) { | ||
themeSwitcher.focus() | ||
} | ||
} | ||
|
||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { | ||
const storedTheme = getStoredTheme() | ||
if (storedTheme !== 'light' && storedTheme !== 'dark') { | ||
setTheme(getPreferredTheme()) | ||
} | ||
}) | ||
|
||
window.addEventListener('DOMContentLoaded', () => { | ||
showActiveTheme(getPreferredTheme()) | ||
|
||
document.querySelectorAll('[data-bs-theme-value]') | ||
.forEach(toggle => { | ||
toggle.addEventListener('click', () => { | ||
const theme = toggle.getAttribute('data-bs-theme-value') | ||
setStoredTheme(theme) | ||
setTheme(theme) | ||
showActiveTheme(theme, true) | ||
}) | ||
}) | ||
}) | ||
})() |
Oops, something went wrong.