oldnews 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oldnews/__main__.py +1 -2
- oldnews/data/__init__.py +6 -5
- oldnews/data/config.py +6 -3
- oldnews/data/last_grab.py +11 -19
- oldnews/data/local_articles.py +107 -246
- oldnews/data/local_data.py +45 -0
- oldnews/data/local_folders.py +12 -21
- oldnews/data/local_subscriptions.py +26 -67
- oldnews/data/local_unread.py +5 -3
- oldnews/data/models/__init__.py +24 -0
- oldnews/data/models/local_article.py +81 -0
- oldnews/data/models/local_folder.py +19 -0
- oldnews/data/models/local_state.py +25 -0
- oldnews/data/models/local_subscription.py +41 -0
- oldnews/data/navigation_state.py +11 -20
- oldnews/data/reset.py +7 -1
- oldnews/oldnews.py +8 -1
- oldnews/screens/information_display.py +1 -1
- oldnews/screens/main.py +55 -47
- oldnews/sync.py +40 -35
- oldnews/widgets/_next_matching_option.py +1 -1
- oldnews/widgets/article_list.py +10 -9
- oldnews/widgets/navigation.py +24 -14
- {oldnews-0.8.0.dist-info → oldnews-0.9.0.dist-info}/METADATA +2 -2
- oldnews-0.9.0.dist-info/RECORD +44 -0
- {oldnews-0.8.0.dist-info → oldnews-0.9.0.dist-info}/WHEEL +1 -1
- oldnews/data/db.py +0 -97
- oldnews/data/tools.py +0 -45
- oldnews-0.8.0.dist-info/RECORD +0 -40
- {oldnews-0.8.0.dist-info → oldnews-0.9.0.dist-info}/entry_points.txt +0 -0
oldnews/screens/main.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Python imports.
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from datetime import datetime, timedelta
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
7
|
from webbrowser import open as open_url
|
|
8
8
|
|
|
9
9
|
##############################################################################
|
|
@@ -363,10 +363,12 @@ class Main(EnhancedScreen[None]):
|
|
|
363
363
|
self.unread = message.counts
|
|
364
364
|
self.post_message(self.SubTitle(""))
|
|
365
365
|
|
|
366
|
-
def _refresh_article_list(self) -> None:
|
|
366
|
+
async def _refresh_article_list(self) -> None:
|
|
367
367
|
"""Refresh the content of the article list."""
|
|
368
368
|
if self.current_category:
|
|
369
|
-
self.articles = get_local_articles(
|
|
369
|
+
self.articles = await get_local_articles(
|
|
370
|
+
self.current_category, not self.show_all
|
|
371
|
+
)
|
|
370
372
|
# If the result is there's nothing showing, tidy up the content
|
|
371
373
|
# side of the display and maybe move focus back to navigation.
|
|
372
374
|
if not self.articles:
|
|
@@ -375,32 +377,32 @@ class Main(EnhancedScreen[None]):
|
|
|
375
377
|
self.navigation.focus()
|
|
376
378
|
self.article_view.set_class(bool(self.articles), "--has-articles")
|
|
377
379
|
|
|
378
|
-
@work(
|
|
379
|
-
def _load_locally(self) -> None:
|
|
380
|
+
@work(exclusive=True)
|
|
381
|
+
async def _load_locally(self) -> None:
|
|
380
382
|
"""Load up any locally-held data."""
|
|
381
|
-
if subscriptions := get_local_subscriptions():
|
|
383
|
+
if subscriptions := await get_local_subscriptions():
|
|
382
384
|
self.post_message(self.NewSubscriptions(subscriptions))
|
|
383
|
-
if folders := get_local_folders():
|
|
385
|
+
if folders := await get_local_folders():
|
|
384
386
|
self.post_message(self.NewFolders(folders))
|
|
385
|
-
if cleaned := clean_old_read_articles(
|
|
387
|
+
if cleaned := await clean_old_read_articles(
|
|
386
388
|
timedelta(days=load_configuration().local_history)
|
|
387
389
|
):
|
|
388
390
|
self.notify(f"Old read articles cleaned from local storage: {cleaned}")
|
|
389
|
-
if unread := get_local_unread(folders, subscriptions):
|
|
391
|
+
if unread := await get_local_unread(folders, subscriptions):
|
|
390
392
|
self.post_message(self.NewUnread(unread))
|
|
391
|
-
self._refresh_article_list()
|
|
393
|
+
await self._refresh_article_list()
|
|
392
394
|
# If we've never grabbed data from ToR before, or if it's been long enough...
|
|
393
|
-
if (last_grabbed := last_grabbed_data_at()) is None or (
|
|
394
|
-
(datetime.now() - last_grabbed).seconds
|
|
395
|
+
if (last_grabbed := await last_grabbed_data_at()) is None or (
|
|
396
|
+
(datetime.now(UTC) - last_grabbed).seconds
|
|
395
397
|
>= load_configuration().startup_refresh_holdoff_period
|
|
396
398
|
):
|
|
397
399
|
# ...kick off a refresh from TheOldReader.
|
|
398
400
|
self.post_message(RefreshFromTheOldReader())
|
|
399
401
|
|
|
400
402
|
@on(SyncFinished)
|
|
401
|
-
def _sync_finished(self) -> None:
|
|
403
|
+
async def _sync_finished(self) -> None:
|
|
402
404
|
"""Clean up after a sync from TheOldReader has finished."""
|
|
403
|
-
self._refresh_article_list()
|
|
405
|
+
await self._refresh_article_list()
|
|
404
406
|
self.post_message(self.SubTitle(""))
|
|
405
407
|
|
|
406
408
|
@on(RefreshFromTheOldReader)
|
|
@@ -422,7 +424,9 @@ class Main(EnhancedScreen[None]):
|
|
|
422
424
|
).sync()
|
|
423
425
|
|
|
424
426
|
@on(Navigation.CategorySelected)
|
|
425
|
-
def _handle_navigaion_selection(
|
|
427
|
+
async def _handle_navigaion_selection(
|
|
428
|
+
self, message: Navigation.CategorySelected
|
|
429
|
+
) -> None:
|
|
426
430
|
"""Handle a navigation selection being made.
|
|
427
431
|
|
|
428
432
|
Args:
|
|
@@ -430,12 +434,12 @@ class Main(EnhancedScreen[None]):
|
|
|
430
434
|
"""
|
|
431
435
|
self.current_category = message.category
|
|
432
436
|
self.article = None
|
|
433
|
-
self._refresh_article_list()
|
|
437
|
+
await self._refresh_article_list()
|
|
434
438
|
self.article_list.focus()
|
|
435
439
|
|
|
436
|
-
def _watch_show_all(self) -> None:
|
|
440
|
+
async def _watch_show_all(self) -> None:
|
|
437
441
|
"""Handle changes to the show all flag."""
|
|
438
|
-
self._refresh_article_list()
|
|
442
|
+
await self._refresh_article_list()
|
|
439
443
|
|
|
440
444
|
@work
|
|
441
445
|
async def _mark_read(self, article: Article) -> None:
|
|
@@ -444,11 +448,11 @@ class Main(EnhancedScreen[None]):
|
|
|
444
448
|
Args:
|
|
445
449
|
article: The article to mark as read.
|
|
446
450
|
"""
|
|
447
|
-
locally_mark_read(article)
|
|
451
|
+
await locally_mark_read(article)
|
|
448
452
|
self.post_message(
|
|
449
|
-
self.NewUnread(get_local_unread(self.folders, self.subscriptions))
|
|
453
|
+
self.NewUnread(await get_local_unread(self.folders, self.subscriptions))
|
|
450
454
|
)
|
|
451
|
-
self._refresh_article_list()
|
|
455
|
+
await self._refresh_article_list()
|
|
452
456
|
await article.mark_read(self._session)
|
|
453
457
|
|
|
454
458
|
@on(ArticleList.ViewArticle)
|
|
@@ -544,7 +548,10 @@ class Main(EnhancedScreen[None]):
|
|
|
544
548
|
]
|
|
545
549
|
):
|
|
546
550
|
return
|
|
547
|
-
category_description =
|
|
551
|
+
category_description = (
|
|
552
|
+
f"{current_category.__class__.__name__.lower()} "
|
|
553
|
+
f"'{current_category.name if isinstance(current_category, Folder) else current_category.title}'"
|
|
554
|
+
)
|
|
548
555
|
plural = "s" if len(ids_to_mark_read) > 1 else ""
|
|
549
556
|
if await self.app.push_screen_wait(
|
|
550
557
|
Confirm(
|
|
@@ -554,11 +561,13 @@ class Main(EnhancedScreen[None]):
|
|
|
554
561
|
)
|
|
555
562
|
):
|
|
556
563
|
if await self._session.add_tag(ids_to_mark_read, State.READ):
|
|
557
|
-
locally_mark_article_ids_read(ids_to_mark_read)
|
|
564
|
+
await locally_mark_article_ids_read(ids_to_mark_read)
|
|
558
565
|
self.post_message(
|
|
559
|
-
self.NewUnread(
|
|
566
|
+
self.NewUnread(
|
|
567
|
+
await get_local_unread(self.folders, self.subscriptions)
|
|
568
|
+
)
|
|
560
569
|
)
|
|
561
|
-
self._refresh_article_list()
|
|
570
|
+
await self._refresh_article_list()
|
|
562
571
|
self.notify(
|
|
563
572
|
f"{len(ids_to_mark_read)} article{plural} marked read for {category_description}"
|
|
564
573
|
)
|
|
@@ -687,7 +696,7 @@ class Main(EnhancedScreen[None]):
|
|
|
687
696
|
ModalInput("Subscription name", folder.name)
|
|
688
697
|
):
|
|
689
698
|
if await Folders.rename(self._session, folder, new_name):
|
|
690
|
-
rename_folder_for_articles(folder, new_name)
|
|
699
|
+
await rename_folder_for_articles(folder, new_name)
|
|
691
700
|
self.notify("Renamed")
|
|
692
701
|
self.post_message(RefreshFromTheOldReader())
|
|
693
702
|
else:
|
|
@@ -711,11 +720,11 @@ class Main(EnhancedScreen[None]):
|
|
|
711
720
|
if await self.app.push_screen_wait(
|
|
712
721
|
Confirm(
|
|
713
722
|
f"Remove {subscription.title}?",
|
|
714
|
-
|
|
723
|
+
"Are you sure you wish to remove the subscription?",
|
|
715
724
|
)
|
|
716
725
|
):
|
|
717
726
|
if await Subscriptions.remove(self._session, subscription):
|
|
718
|
-
remove_subscription_articles(subscription)
|
|
727
|
+
await remove_subscription_articles(subscription)
|
|
719
728
|
self.notify(f"Removed {subscription.title}")
|
|
720
729
|
self.post_message(RefreshFromTheOldReader())
|
|
721
730
|
else:
|
|
@@ -734,7 +743,7 @@ class Main(EnhancedScreen[None]):
|
|
|
734
743
|
)
|
|
735
744
|
):
|
|
736
745
|
if await Folders.remove(self._session, folder):
|
|
737
|
-
remove_folder_from_articles(folder)
|
|
746
|
+
await remove_folder_from_articles(folder)
|
|
738
747
|
self.notify(f"Removed {folder.name}")
|
|
739
748
|
self.post_message(RefreshFromTheOldReader())
|
|
740
749
|
else:
|
|
@@ -751,25 +760,24 @@ class Main(EnhancedScreen[None]):
|
|
|
751
760
|
@work
|
|
752
761
|
async def action_move_subscription_command(self) -> None:
|
|
753
762
|
"""Move a subscription to a different folder."""
|
|
754
|
-
if subscription := self.navigation.current_subscription:
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
763
|
+
if not (subscription := self.navigation.current_subscription):
|
|
764
|
+
return
|
|
765
|
+
if (
|
|
766
|
+
target_folder := await self.app.push_screen_wait(FolderInput(self.folders))
|
|
767
|
+
) is not None:
|
|
768
|
+
if await Subscriptions.move(self._session, subscription, target_folder):
|
|
769
|
+
await move_subscription_articles(
|
|
770
|
+
subscription, subscription.folder_id, target_folder
|
|
771
|
+
)
|
|
772
|
+
self.notify("Moved")
|
|
773
|
+
self.post_message(RefreshFromTheOldReader())
|
|
774
|
+
else:
|
|
775
|
+
self.notify(
|
|
776
|
+
f"Could not move the subscription into '{target_folder}'",
|
|
777
|
+
title="Move failed",
|
|
778
|
+
timeout=8,
|
|
779
|
+
markup=False,
|
|
758
780
|
)
|
|
759
|
-
) is not None:
|
|
760
|
-
if await Subscriptions.move(self._session, subscription, target_folder):
|
|
761
|
-
move_subscription_articles(
|
|
762
|
-
subscription, subscription.folder_id, target_folder
|
|
763
|
-
)
|
|
764
|
-
self.notify("Moved")
|
|
765
|
-
self.post_message(RefreshFromTheOldReader())
|
|
766
|
-
else:
|
|
767
|
-
self.notify(
|
|
768
|
-
f"Could not move the subscription into '{target_folder}'",
|
|
769
|
-
title="Move failed",
|
|
770
|
-
timeout=8,
|
|
771
|
-
markup=False,
|
|
772
|
-
)
|
|
773
781
|
|
|
774
782
|
@work
|
|
775
783
|
async def action_information_command(self) -> None:
|
oldnews/sync.py
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Python imports.
|
|
5
|
+
from collections.abc import AsyncIterator, Callable, Iterable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
|
-
from datetime import datetime, timedelta
|
|
7
|
-
from typing import Any
|
|
7
|
+
from datetime import UTC, datetime, timedelta
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
##############################################################################
|
|
10
11
|
# OldAS imports.
|
|
@@ -41,10 +42,6 @@ type Callback = Callable[[], Any] | None
|
|
|
41
42
|
type CallbackWith[T] = Callable[[T], Any] | None
|
|
42
43
|
"""Type of callback with a single argument."""
|
|
43
44
|
|
|
44
|
-
##############################################################################
|
|
45
|
-
BATCH_SIZE: Final[int] = 10
|
|
46
|
-
"""Batch size for downloading articles."""
|
|
47
|
-
|
|
48
45
|
|
|
49
46
|
##############################################################################
|
|
50
47
|
@dataclass
|
|
@@ -68,9 +65,11 @@ class TheOldReaderSync:
|
|
|
68
65
|
|
|
69
66
|
def __post_init__(self) -> None:
|
|
70
67
|
"""Initialise the sync object."""
|
|
71
|
-
self.
|
|
68
|
+
self._batch_size = load_configuration().article_download_batch_size
|
|
69
|
+
"""The size of the article batch to download and save."""
|
|
70
|
+
self._last_sync: datetime | None = None
|
|
72
71
|
"""The time at which we last did a sync."""
|
|
73
|
-
self._first_sync =
|
|
72
|
+
self._first_sync = True
|
|
74
73
|
"""Is this our first ever sync?"""
|
|
75
74
|
|
|
76
75
|
def _step(self, step: str, *, log: bool = True) -> None:
|
|
@@ -106,21 +105,25 @@ class TheOldReaderSync:
|
|
|
106
105
|
The number of articles downloaded.
|
|
107
106
|
"""
|
|
108
107
|
loaded = 0
|
|
108
|
+
save_batch: list[Article] = []
|
|
109
109
|
async for article in stream:
|
|
110
110
|
# I've encountered articles that don't have an origin stream ID,
|
|
111
111
|
# which means that I can't relate them back to a stream, which
|
|
112
112
|
# means I'll never see them anyway...
|
|
113
113
|
if not article.origin.stream_id:
|
|
114
114
|
continue
|
|
115
|
-
|
|
116
|
-
# should save them in small batches? This would be simple enough
|
|
117
|
-
# -- perhaps same them in batches the same size as the buffer
|
|
118
|
-
# window I'm using right now (currently BATCH_SIZE articles per
|
|
119
|
-
# trip to ToR).
|
|
120
|
-
save_local_articles(Articles([article]))
|
|
115
|
+
save_batch.append(article)
|
|
121
116
|
loaded += 1
|
|
122
|
-
if (loaded %
|
|
117
|
+
if (loaded % self._batch_size) == 0:
|
|
123
118
|
self._step(f"{description}: {loaded}", log=False)
|
|
119
|
+
Log().debug(f"Saving batch of articles: {len(save_batch)}")
|
|
120
|
+
await save_local_articles(Articles(save_batch))
|
|
121
|
+
Log().debug(f"Saved batch of articles: {len(save_batch)}")
|
|
122
|
+
save_batch = []
|
|
123
|
+
if save_batch:
|
|
124
|
+
Log().debug(f"Saving final batch of articles: {len(save_batch)}")
|
|
125
|
+
await save_local_articles(Articles(save_batch))
|
|
126
|
+
Log().debug(f"Saved final batch of articles: {len(save_batch)}")
|
|
124
127
|
return loaded
|
|
125
128
|
|
|
126
129
|
async def _get_updated_read_status(self) -> None:
|
|
@@ -128,14 +131,14 @@ class TheOldReaderSync:
|
|
|
128
131
|
if self._first_sync:
|
|
129
132
|
return
|
|
130
133
|
self._step("Syncing read/unread status with TheOldReader")
|
|
131
|
-
remote_unread_articles =
|
|
134
|
+
remote_unread_articles = {
|
|
132
135
|
article_id.full_id
|
|
133
136
|
for article_id in await ArticleIDs.load_unread(self.session)
|
|
134
|
-
|
|
135
|
-
local_unread_articles = set(get_unread_article_ids())
|
|
137
|
+
}
|
|
138
|
+
local_unread_articles = set(await get_unread_article_ids())
|
|
136
139
|
if mark_as_read := local_unread_articles - remote_unread_articles:
|
|
137
140
|
Log().debug(f"Articles found as marked read elsewhere: {mark_as_read}")
|
|
138
|
-
locally_mark_article_ids_read(mark_as_read)
|
|
141
|
+
await locally_mark_article_ids_read(mark_as_read)
|
|
139
142
|
self._result(
|
|
140
143
|
f"Articles found read elsewhere on TheOldReader: {len(mark_as_read)}"
|
|
141
144
|
)
|
|
@@ -146,13 +149,11 @@ class TheOldReaderSync:
|
|
|
146
149
|
Args:
|
|
147
150
|
subscriptions: The subscriptions to download the backlog for.
|
|
148
151
|
"""
|
|
149
|
-
cutoff = datetime.now(
|
|
150
|
-
days=load_configuration().local_history
|
|
151
|
-
)
|
|
152
|
+
cutoff = datetime.now(UTC) - timedelta(days=load_configuration().local_history)
|
|
152
153
|
for subscription in subscriptions:
|
|
153
154
|
if loaded := await self._download(
|
|
154
155
|
Articles.stream_new_since(
|
|
155
|
-
self.session, cutoff, subscription, n=
|
|
156
|
+
self.session, cutoff, subscription, n=self._batch_size
|
|
156
157
|
),
|
|
157
158
|
f"Downloading article backlog for {subscription.title}",
|
|
158
159
|
):
|
|
@@ -167,7 +168,7 @@ class TheOldReaderSync:
|
|
|
167
168
|
The folders.
|
|
168
169
|
"""
|
|
169
170
|
self._step("Getting folder list")
|
|
170
|
-
folders = save_local_folders(await Folders.load(self.session))
|
|
171
|
+
folders = await save_local_folders(await Folders.load(self.session))
|
|
171
172
|
if self.on_new_folders:
|
|
172
173
|
self.on_new_folders(folders)
|
|
173
174
|
return folders
|
|
@@ -180,8 +181,10 @@ class TheOldReaderSync:
|
|
|
180
181
|
after.
|
|
181
182
|
"""
|
|
182
183
|
self._step("Getting subscriptions list")
|
|
183
|
-
original_subscriptions = get_local_subscriptions()
|
|
184
|
-
subscriptions =
|
|
184
|
+
original_subscriptions = await get_local_subscriptions()
|
|
185
|
+
subscriptions = await save_local_subscriptions(
|
|
186
|
+
await Subscriptions.load(self.session)
|
|
187
|
+
)
|
|
185
188
|
if self.on_new_subscriptions:
|
|
186
189
|
self.on_new_subscriptions(subscriptions)
|
|
187
190
|
return original_subscriptions, subscriptions
|
|
@@ -193,18 +196,18 @@ class TheOldReaderSync:
|
|
|
193
196
|
if self._first_sync
|
|
194
197
|
else f"Getting new articles since {self._last_sync}"
|
|
195
198
|
)
|
|
196
|
-
new_grab = datetime.now(
|
|
199
|
+
new_grab = datetime.now(UTC)
|
|
197
200
|
last_grabbed = self._last_sync or (
|
|
198
201
|
new_grab - timedelta(days=load_configuration().local_history)
|
|
199
202
|
)
|
|
200
203
|
if loaded := await self._download(
|
|
201
|
-
Articles.stream_new_since(self.session, last_grabbed, n=
|
|
204
|
+
Articles.stream_new_since(self.session, last_grabbed, n=self._batch_size),
|
|
202
205
|
"Downloading articles from TheOldReader",
|
|
203
206
|
):
|
|
204
207
|
self._result(f"Articles downloaded: {loaded}")
|
|
205
208
|
else:
|
|
206
209
|
self._result("No new articles found on TheOldReader")
|
|
207
|
-
remember_we_last_grabbed_at(new_grab)
|
|
210
|
+
await remember_we_last_grabbed_at(new_grab)
|
|
208
211
|
|
|
209
212
|
@staticmethod
|
|
210
213
|
def _set_of_ids(subscriptions: Subscriptions) -> set[str]:
|
|
@@ -247,7 +250,7 @@ class TheOldReaderSync:
|
|
|
247
250
|
if subscription.id in new_subscriptions
|
|
248
251
|
)
|
|
249
252
|
|
|
250
|
-
def _clean_orphaned_articles(
|
|
253
|
+
async def _clean_orphaned_articles(
|
|
251
254
|
self,
|
|
252
255
|
original_subscriptions: Subscriptions,
|
|
253
256
|
current_subscriptions: Subscriptions,
|
|
@@ -265,9 +268,9 @@ class TheOldReaderSync:
|
|
|
265
268
|
):
|
|
266
269
|
Log().info(f"Found remotely-removed subscriptions: {removed_subscriptions}")
|
|
267
270
|
for subscription in removed_subscriptions:
|
|
268
|
-
remove_subscription_articles(subscription)
|
|
271
|
+
await remove_subscription_articles(subscription)
|
|
269
272
|
|
|
270
|
-
def _get_unread_counts(
|
|
273
|
+
async def _get_unread_counts(
|
|
271
274
|
self, folders: Folders, subscriptions: Subscriptions
|
|
272
275
|
) -> None:
|
|
273
276
|
"""Get the updated unread counts.
|
|
@@ -277,17 +280,19 @@ class TheOldReaderSync:
|
|
|
277
280
|
subscriptions: The subscriptions to get the counts for.
|
|
278
281
|
"""
|
|
279
282
|
if self.on_new_unread:
|
|
280
|
-
self.on_new_unread(get_local_unread(folders, subscriptions))
|
|
283
|
+
self.on_new_unread(await get_local_unread(folders, subscriptions))
|
|
281
284
|
|
|
282
285
|
async def sync(self) -> None:
|
|
283
286
|
"""Sync the data from TheOldReader."""
|
|
287
|
+
self._last_sync = await last_grabbed_data_at()
|
|
288
|
+
self._first_sync = self._last_sync is None
|
|
284
289
|
folders = await self._get_folders()
|
|
285
290
|
original_subscriptions, subscriptions = await self._get_subscriptions()
|
|
286
291
|
await self._get_new_articles()
|
|
287
292
|
await self._get_updated_read_status()
|
|
288
293
|
await self._get_historical_articles(original_subscriptions, subscriptions)
|
|
289
|
-
self._clean_orphaned_articles(original_subscriptions, subscriptions)
|
|
290
|
-
self._get_unread_counts(folders, subscriptions)
|
|
294
|
+
await self._clean_orphaned_articles(original_subscriptions, subscriptions)
|
|
295
|
+
await self._get_unread_counts(folders, subscriptions)
|
|
291
296
|
if self.on_sync_finished:
|
|
292
297
|
self.on_sync_finished()
|
|
293
298
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
##############################################################################
|
|
4
4
|
# Python imports.
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Callable
|
|
6
6
|
|
|
7
7
|
##############################################################################
|
|
8
8
|
# BagOfStuff imports.
|
oldnews/widgets/article_list.py
CHANGED
|
@@ -153,15 +153,16 @@ class ArticleList(EnhancedOptionList):
|
|
|
153
153
|
`True` if an unread article was found and highlighted, `False`
|
|
154
154
|
if not.
|
|
155
155
|
"""
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
if (
|
|
157
|
+
next_hit := next_matching_option(
|
|
158
|
+
cast(list[ArticleView], self.options),
|
|
159
|
+
self.highlighted,
|
|
160
|
+
direction,
|
|
161
|
+
attrgetter("article.is_unread"),
|
|
162
|
+
)
|
|
163
|
+
) and next_hit.id is not None:
|
|
164
|
+
self.highlighted = self.get_option_index(next_hit.id)
|
|
165
|
+
return True
|
|
165
166
|
self.notify("No more unread articles")
|
|
166
167
|
return False
|
|
167
168
|
|
oldnews/widgets/navigation.py
CHANGED
|
@@ -6,8 +6,9 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
##############################################################################
|
|
8
8
|
# Python imports.
|
|
9
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
9
10
|
from dataclasses import dataclass
|
|
10
|
-
from typing import
|
|
11
|
+
from typing import cast
|
|
11
12
|
|
|
12
13
|
##############################################################################
|
|
13
14
|
# OldAs imports.
|
|
@@ -154,9 +155,13 @@ class Navigation(EnhancedOptionList):
|
|
|
154
155
|
classes: The CSS classes of the navigation widget.
|
|
155
156
|
"""
|
|
156
157
|
super().__init__(id=id, classes=classes)
|
|
157
|
-
self._expanded =
|
|
158
|
+
self._expanded: set[str] = set()
|
|
158
159
|
"""The IDs of the folders that are expanded."""
|
|
159
160
|
|
|
161
|
+
def on_mount(self) -> None:
|
|
162
|
+
"""Configure the widget once the DOM is mounted."""
|
|
163
|
+
self.call_next(self._load_state)
|
|
164
|
+
|
|
160
165
|
@staticmethod
|
|
161
166
|
def _key(attr: str) -> Callable[[object], str]:
|
|
162
167
|
"""Create a key to use with `sorted`.
|
|
@@ -250,14 +255,18 @@ class Navigation(EnhancedOptionList):
|
|
|
250
255
|
"""React to the unread data being updated."""
|
|
251
256
|
self._refresh_navigation()
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
|
|
258
|
+
async def _load_state(self) -> None:
|
|
259
|
+
"""Load the navigation state."""
|
|
260
|
+
self._expanded = await get_navigation_state()
|
|
261
|
+
|
|
262
|
+
@work
|
|
263
|
+
async def _save_state(self, state: set[str]) -> None:
|
|
255
264
|
"""Save the folder expanded/collapsed state.
|
|
256
265
|
|
|
257
266
|
Args:
|
|
258
267
|
state: The state to save.
|
|
259
268
|
"""
|
|
260
|
-
save_navigation_state(state)
|
|
269
|
+
await save_navigation_state(state)
|
|
261
270
|
|
|
262
271
|
def _set_expansion(self, new_state: set[str]) -> None:
|
|
263
272
|
"""Set the new navigation state.
|
|
@@ -349,15 +358,16 @@ class Navigation(EnhancedOptionList):
|
|
|
349
358
|
`True` if an unread category was found and highlighted, `False`
|
|
350
359
|
if not.
|
|
351
360
|
"""
|
|
352
|
-
if
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
+
if (
|
|
362
|
+
next_hit := next_matching_option(
|
|
363
|
+
cast(list[FolderView | SubscriptionView], self.options),
|
|
364
|
+
self.highlighted,
|
|
365
|
+
direction,
|
|
366
|
+
self._contains_unread,
|
|
367
|
+
)
|
|
368
|
+
) and next_hit.id is not None:
|
|
369
|
+
self.highlighted = self.get_option_index(next_hit.id)
|
|
370
|
+
return True
|
|
361
371
|
self.notify("No more folders or subscriptions with unread articles")
|
|
362
372
|
return False
|
|
363
373
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oldnews
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A terminal-based client for TheOldReader
|
|
5
5
|
Keywords: atom,client,Google Reader,RSS,TheOldReader,terminal,news-reader,news
|
|
6
6
|
Author: Dave Pearson
|
|
@@ -20,7 +20,7 @@ Requires-Dist: oldas>=0.10.0
|
|
|
20
20
|
Requires-Dist: textual>=6.3.0
|
|
21
21
|
Requires-Dist: textual-autocomplete>=4.0.6
|
|
22
22
|
Requires-Dist: textual-enhanced>=1.2.0
|
|
23
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: tortoise-orm>=0.25.4
|
|
24
24
|
Requires-Dist: xdg-base-dirs>=6.0.2
|
|
25
25
|
Requires-Python: >=3.12
|
|
26
26
|
Project-URL: Homepage, https://github.com/davep/oldnews
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
oldnews/__init__.py,sha256=p5a3l8kly6VYQ4IjiC6s03Nj-FaLDTOzqU1ZC6WZLl0,540
|
|
2
|
+
oldnews/__main__.py,sha256=ly786aBy3mi6DxDAimvHJ3QFjj-y8-7EKD_0F8YXf4Q,4648
|
|
3
|
+
oldnews/commands/__init__.py,sha256=R8w9ccEYqZ4AKdtg2vvT_ejgdC3rsQ7yiTVnYVWDLOQ,1042
|
|
4
|
+
oldnews/commands/main.py,sha256=lPfOIYwf8L7eYG9GUy2MDvoj4fl3ZikjY8Ld-dmv7i0,4089
|
|
5
|
+
oldnews/data/__init__.py,sha256=vzka2owD1vv63iDFU2SXXirzOPELHcfLVRWmCnegnTI,2158
|
|
6
|
+
oldnews/data/auth.py,sha256=5CWu4kjEkyJ8TDqqIYkUNFAzebygpksKIPdFhT62uOM,1217
|
|
7
|
+
oldnews/data/config.py,sha256=bxy-rN-GKqdg2Lxh4exRL3pvzqiqt-YFZdISFoM8poE,3441
|
|
8
|
+
oldnews/data/dump.py,sha256=sgWz1HHJlxG_B8uDtwtt_TS1ulnP4yji3hK7iinMc7Q,2206
|
|
9
|
+
oldnews/data/last_grab.py,sha256=hiXU4AEpi0U1ED4T8HaClZOZfcv1rIytDDsd3Ll3WzY,1384
|
|
10
|
+
oldnews/data/local_articles.py,sha256=kEGKxP7p7Xu3fbYoKLWrXAwv_OU3ausym4yUuheUJ_U,10894
|
|
11
|
+
oldnews/data/local_data.py,sha256=vhfJ8-GYHf_n-kQV4fII-4tHKSRVBakFNQP0fww2ByQ,1359
|
|
12
|
+
oldnews/data/local_folders.py,sha256=JTojYhRdx4q8d_ngoaeWVMsJXEZKhbHqKqxFDmWBdww,1387
|
|
13
|
+
oldnews/data/local_subscriptions.py,sha256=UngKYLzK6umsScSwumc2P4634KqhOawkYJD8guOOMlM,2714
|
|
14
|
+
oldnews/data/local_unread.py,sha256=TcjEeU4QRZRh1gjgDSL239nolgOOlLgWkO8zeRsBPn0,1466
|
|
15
|
+
oldnews/data/locations.py,sha256=rJqXy_15ok5G86XbrXmixyYTBLFO0V0BWD8rLKotuBg,1723
|
|
16
|
+
oldnews/data/log.py,sha256=nd6w7NAiUzZjeVM4HZd-oWFFVyQvWblXjdRYlSjua5k,1211
|
|
17
|
+
oldnews/data/models/__init__.py,sha256=nRwT1Nrh30x8Hed_327Qvslkus8faUQTlQ8ToYTN57Q,718
|
|
18
|
+
oldnews/data/models/local_article.py,sha256=0tiR1lpeR54zqMGqzvVsNhfghGUaIU1jH9rX39lquPk,3043
|
|
19
|
+
oldnews/data/models/local_folder.py,sha256=ZOq1kKBne_Hjewddkm2T7VidJ2vGR41F5QzoJ-rfotA,586
|
|
20
|
+
oldnews/data/models/local_state.py,sha256=-4JWUdxBSuT5RhNOwI0HwaFOlj_QWMyJoIPCLGtG7wU,798
|
|
21
|
+
oldnews/data/models/local_subscription.py,sha256=TkNa20lL79j_JCq5zT2vYXqjG9EmMZP_GN0km8XgGsE,1533
|
|
22
|
+
oldnews/data/navigation_state.py,sha256=nEVC1BxT-BofbSesDraCfmsFufeX2lBbqEct17zY4P8,1106
|
|
23
|
+
oldnews/data/reset.py,sha256=VNItdDmiDnnueroM1errUdL77yMKJY2_ZZkfZHkXeoI,825
|
|
24
|
+
oldnews/oldnews.py,sha256=8LNb8fEH5VDmt0GJqHtWdHSRlYvZ6v8ddMgWrpCpd2A,4088
|
|
25
|
+
oldnews/providers/__init__.py,sha256=R_wS987EuI3KDiN9Xzf0P5wiRSYLOk_-3twvr2ML7WU,354
|
|
26
|
+
oldnews/providers/main.py,sha256=kHyXfumWV6pZhLUfAe6LCsEYQmITYmV0RxXIn0qmlNs,2020
|
|
27
|
+
oldnews/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
oldnews/screens/__init__.py,sha256=-Iif16T316dnZkgJjX1YE7e4v7FxI0k_V08qd5za0t8,330
|
|
29
|
+
oldnews/screens/folder_input.py,sha256=i6KeRVD1Ucfg2T6VJmzdKKnUHf6qo_1ka0ZLlNMuabA,1869
|
|
30
|
+
oldnews/screens/information_display.py,sha256=D2H7k9NjvyytLGk4FXs169VMV-ajG3UwFrUzdfQhvKM,2552
|
|
31
|
+
oldnews/screens/login.py,sha256=a2hUERxpKYg7GQIYlpyDgHTAUEKmsANjwHZx1r-gXSw,3377
|
|
32
|
+
oldnews/screens/main.py,sha256=S3nrIE9ZgGw1qOD-3tWhQ-CYSRCwT8nQn1ZP9GR7arw,28963
|
|
33
|
+
oldnews/screens/new_subscription.py,sha256=bk01RXWu3kvJbtm88o1mrYpll73Op3PWWl7ggIH29kk,4011
|
|
34
|
+
oldnews/screens/process_subscription.py,sha256=8Z7aJLx-KkItaCQXfgf44cOQ83AZPlRJcMxFDuaHk54,2891
|
|
35
|
+
oldnews/sync.py,sha256=3IbfqWwzMfVK6Uy-651hn2_PCUunthH25-nhPXhJiLk,11491
|
|
36
|
+
oldnews/widgets/__init__.py,sha256=5VSjKswHxv2W8g0O-LWlUFS-gy7iQrIM8PGvSBhDNLU,438
|
|
37
|
+
oldnews/widgets/_next_matching_option.py,sha256=ZXBXiQCZHc-m6yz4xH63onvDUkxnPalgjZdk55DPEAo,1609
|
|
38
|
+
oldnews/widgets/article_content.py,sha256=pv8zobd3eTsALHYmP854dd_enqy32QXGoWXEILYnWfA,3553
|
|
39
|
+
oldnews/widgets/article_list.py,sha256=bbp8J-9Hw3_xRObkqLjIg7_sJp14pK6soG2qMtFXrSQ,7396
|
|
40
|
+
oldnews/widgets/navigation.py,sha256=ItK8genhD2zYTkaOONSF4wR9tScu8oVDQziTzXu4qAI,13012
|
|
41
|
+
oldnews-0.9.0.dist-info/WHEEL,sha256=5DEXXimM34_d4Gx1AuF9ysMr1_maoEtGKjaILM3s4w4,80
|
|
42
|
+
oldnews-0.9.0.dist-info/entry_points.txt,sha256=FxY6Y4IsHZubhtdd0QJG3p2kCTcXe0e-Ib_AW0qkotE,51
|
|
43
|
+
oldnews-0.9.0.dist-info/METADATA,sha256=_m8xdUNW56bLykPeS7iM7-FbhgZeMAWhcBFvdh21Kd0,2957
|
|
44
|
+
oldnews-0.9.0.dist-info/RECORD,,
|