oldnews 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (33) hide show
  1. {oldnews-0.2.0 → oldnews-0.3.0}/PKG-INFO +2 -2
  2. {oldnews-0.2.0 → oldnews-0.3.0}/pyproject.toml +2 -2
  3. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/__main__.py +67 -24
  4. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/__init__.py +2 -0
  5. oldnews-0.3.0/src/oldnews/data/reset.py +26 -0
  6. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/screens/main.py +28 -97
  7. oldnews-0.3.0/src/oldnews/sync/__init__.py +11 -0
  8. oldnews-0.3.0/src/oldnews/sync/sync.py +158 -0
  9. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/widgets/article_content.py +17 -2
  10. {oldnews-0.2.0 → oldnews-0.3.0}/README.md +0 -0
  11. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/__init__.py +0 -0
  12. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/commands/__init__.py +0 -0
  13. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/commands/main.py +0 -0
  14. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/auth.py +0 -0
  15. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/config.py +0 -0
  16. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/db.py +0 -0
  17. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/last_grab.py +0 -0
  18. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/local_articles.py +0 -0
  19. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/local_folders.py +0 -0
  20. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/local_subscriptions.py +0 -0
  21. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/local_unread.py +0 -0
  22. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/locations.py +0 -0
  23. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/data/navigation_state.py +0 -0
  24. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/oldnews.py +0 -0
  25. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/providers/__init__.py +0 -0
  26. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/providers/main.py +0 -0
  27. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/py.typed +0 -0
  28. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/screens/__init__.py +0 -0
  29. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/screens/login.py +0 -0
  30. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/widgets/__init__.py +0 -0
  31. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/widgets/_after_highlight.py +0 -0
  32. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/widgets/article_list.py +0 -0
  33. {oldnews-0.2.0 → oldnews-0.3.0}/src/oldnews/widgets/navigation.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oldnews
3
- Version: 0.2.0
3
+ Version: 0.3.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
7
7
  Author-email: Dave Pearson <davep@davep.org>
8
8
  License-Expression: GPL-3.0-or-later
9
- Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3 :: Only
12
12
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oldnews"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "A terminal-based client for TheOldReader"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -27,7 +27,7 @@ dependencies = [
27
27
  "xdg-base-dirs>=6.0.2",
28
28
  ]
29
29
  classifiers = [
30
- "Development Status :: 3 - Alpha",
30
+ "Development Status :: 4 - Beta",
31
31
  "Operating System :: OS Independent",
32
32
  "Programming Language :: Python :: 3 :: Only",
33
33
  "Programming Language :: Python :: 3",
@@ -9,7 +9,7 @@ from operator import attrgetter
9
9
  ##############################################################################
10
10
  # Local imports.
11
11
  from . import __doc__, __version__
12
- from .data import initialise_database
12
+ from .data import initialise_database, reset_data
13
13
  from .oldnews import OldNews
14
14
 
15
15
 
@@ -37,27 +37,48 @@ def get_args() -> Namespace:
37
37
  version=f"%(prog)s v{__version__}",
38
38
  )
39
39
 
40
- # Add --license
40
+ # Add --theme
41
41
  parser.add_argument(
42
- "--license",
43
- "--licence",
42
+ "-t",
43
+ "--theme",
44
+ help="Set the theme for the application (see `themes` command for available themes)",
45
+ )
46
+
47
+ # Allow for commands on the command line.
48
+ sub_parser = parser.add_subparsers(
49
+ dest="command", help="Available commands", required=False
50
+ )
51
+
52
+ # Add the 'license' command.
53
+ sub_parser.add_parser(
54
+ "license",
55
+ aliases=["licence"],
44
56
  help="Show license information",
45
- action="store_true",
46
57
  )
47
58
 
48
- # Add --bindings
49
- parser.add_argument(
50
- "-b",
51
- "--bindings",
59
+ # Add the 'bindings' command.
60
+ sub_parser.add_parser(
61
+ "bindings",
52
62
  help="List commands that can have their bindings changed",
53
- action="store_true",
54
63
  )
55
64
 
56
- # Add --theme
57
- parser.add_argument(
58
- "-t",
59
- "--theme",
60
- help="Set the theme for the application (set to ? to list available themes)",
65
+ # Add the 'themes' command.
66
+ sub_parser.add_parser(
67
+ "themes", help="List the available themes that can be used with --theme"
68
+ )
69
+
70
+ # Add the 'reset' command.
71
+ reset = sub_parser.add_parser(
72
+ "reset", help="Remove all data downloaded from TheOldReader"
73
+ )
74
+ reset.add_argument(
75
+ "-l", "--logout", help="Force a logout from TheOldReader", action="store_true"
76
+ )
77
+ reset.add_argument(
78
+ "-y",
79
+ "--yes",
80
+ help="Perform the reset without confirming first",
81
+ action="store_true",
61
82
  )
62
83
 
63
84
  # Finally, parse the command line.
@@ -91,18 +112,40 @@ def show_themes() -> None:
91
112
  print(theme)
92
113
 
93
114
 
115
+ ##############################################################################
116
+ def reset_news(args: Namespace) -> None:
117
+ """Perform a reset on the news data.
118
+
119
+ Args:
120
+ args: The command line arguments.
121
+ """
122
+ from rich.prompt import Confirm
123
+
124
+ logout = " and log you out" if args.logout else ""
125
+ if args.yes or Confirm().ask(
126
+ f"This will erase all the local news data{logout}; are you sure?", default=False
127
+ ):
128
+ reset_data(args.logout)
129
+ print("Local data erased")
130
+ if args.logout:
131
+ print("Login token removed")
132
+
133
+
94
134
  ##############################################################################
95
135
  def main() -> None:
96
136
  """Main entry function."""
97
- if (args := get_args()).license:
98
- print(cleandoc(OldNews.HELP_LICENSE))
99
- elif args.bindings:
100
- show_bindable_commands()
101
- elif args.theme == "?":
102
- show_themes()
103
- else:
104
- initialise_database()
105
- OldNews(args).run()
137
+ match (args := get_args()).command:
138
+ case "reset":
139
+ reset_news(args)
140
+ case "license" | "licence":
141
+ print(cleandoc(OldNews.HELP_LICENSE))
142
+ case "bindings":
143
+ show_bindable_commands()
144
+ case "themes":
145
+ show_themes()
146
+ case _:
147
+ initialise_database()
148
+ OldNews(args).run()
106
149
 
107
150
 
108
151
  ### __main__.py ends here
@@ -23,6 +23,7 @@ from .local_folders import get_local_folders, save_local_folders
23
23
  from .local_subscriptions import get_local_subscriptions, save_local_subscriptions
24
24
  from .local_unread import LocalUnread, get_local_unread, total_unread
25
25
  from .navigation_state import get_navigation_state, save_navigation_state
26
+ from .reset import reset_data
26
27
 
27
28
  ##############################################################################
28
29
  # Exports.
@@ -43,6 +44,7 @@ __all__ = [
43
44
  "locally_mark_article_ids_read",
44
45
  "LocalUnread",
45
46
  "remember_we_last_grabbed_at",
47
+ "reset_data",
46
48
  "save_configuration",
47
49
  "save_local_articles",
48
50
  "save_local_folders",
@@ -0,0 +1,26 @@
1
+ """Provides a tool to reset all the data."""
2
+
3
+ ##############################################################################
4
+ # Python imports.
5
+ from pathlib import Path
6
+
7
+ ##############################################################################
8
+ # Local imports.
9
+ from .locations import data_dir
10
+
11
+
12
+ ##############################################################################
13
+ def reset_data(logout: bool) -> None:
14
+ """Erase all the data.
15
+
16
+ Args:
17
+ logout: Should any token file be removed too?
18
+ """
19
+ to_remove: list[Path] = []
20
+ for pattern in ("*.table", "*.db", "*.log", *((".token",) if logout else ())):
21
+ to_remove.extend(data_dir().glob(pattern))
22
+ for data_file in to_remove:
23
+ data_file.unlink()
24
+
25
+
26
+ ### reset.py ends here
@@ -3,14 +3,13 @@
3
3
  ##############################################################################
4
4
  # Python imports.
5
5
  from dataclasses import dataclass
6
- from datetime import datetime, timedelta, timezone
6
+ from datetime import datetime, timedelta
7
7
  from webbrowser import open as open_url
8
8
 
9
9
  ##############################################################################
10
10
  # OldAs imports.
11
11
  from oldas import (
12
12
  Article,
13
- ArticleIDs,
14
13
  Articles,
15
14
  Folder,
16
15
  Folders,
@@ -57,19 +56,15 @@ from ..data import (
57
56
  get_local_folders,
58
57
  get_local_subscriptions,
59
58
  get_local_unread,
60
- get_unread_article_ids,
61
59
  last_grabbed_data_at,
62
60
  load_configuration,
63
61
  locally_mark_article_ids_read,
64
62
  locally_mark_read,
65
- remember_we_last_grabbed_at,
66
- save_local_articles,
67
- save_local_folders,
68
- save_local_subscriptions,
69
63
  total_unread,
70
64
  update_configuration,
71
65
  )
72
66
  from ..providers import MainCommands
67
+ from ..sync import ToRSync
73
68
  from ..widgets import ArticleContent, ArticleList, Navigation
74
69
 
75
70
 
@@ -198,6 +193,9 @@ class Main(EnhancedScreen[None]):
198
193
  counts: LocalUnread
199
194
  """The new unread counts."""
200
195
 
196
+ class SyncFinished(Message):
197
+ """Message sent when a sync from TheOldReader is finished."""
198
+
201
199
  def __init__(self, session: Session) -> None:
202
200
  """Initialise the main screen."""
203
201
  super().__init__()
@@ -298,6 +296,11 @@ class Main(EnhancedScreen[None]):
298
296
  self.unread = message.counts
299
297
  self.post_message(self.SubTitle(""))
300
298
 
299
+ def _refresh_article_list(self) -> None:
300
+ """Refresh the content of the article list."""
301
+ if self.current_category:
302
+ self.articles = get_local_articles(self.current_category, not self.show_all)
303
+
301
304
  @work(thread=True, exclusive=True)
302
305
  def _load_locally(self) -> None:
303
306
  """Load up any locally-held data."""
@@ -311,6 +314,7 @@ class Main(EnhancedScreen[None]):
311
314
  self.notify(f"Old read articles cleaned from local storage: {cleaned}")
312
315
  if unread := get_local_unread(folders, subscriptions):
313
316
  self.post_message(self.NewUnread(unread))
317
+ self._refresh_article_list()
314
318
  # If we've never grabbed data from ToR before, or if it's been long enough...
315
319
  if (last_grabbed := last_grabbed_data_at()) is None or (
316
320
  (datetime.now() - last_grabbed).seconds
@@ -319,95 +323,27 @@ class Main(EnhancedScreen[None]):
319
323
  # ...kick off a refresh from TheOldReader.
320
324
  self.post_message(RefreshFromTheOldReader())
321
325
 
322
- async def _download_newest_articles(self) -> None:
323
- """Download the latest articles available."""
324
- last_grabbed = last_grabbed_data_at() or (
325
- datetime.now() - timedelta(days=load_configuration().local_history)
326
- )
327
- new_grab = datetime.now(timezone.utc)
328
- loaded = 0
329
- async for article in Articles.stream_new_since(
330
- self._session, last_grabbed, n=10
331
- ):
332
- # I've encountered articles that don't have an origin stream ID,
333
- # which means that I can't relate them back to a stream, which
334
- # means I'll never see them anyway...
335
- if not article.origin.stream_id:
336
- continue
337
- # TODO: Right now I'm saving articles one at a time; perhaps I
338
- # should save them in small batches? This would be simple enough
339
- # -- perhaps same them in batches the same size as the buffer
340
- # window I'm using right now (currently 10 articles per trip to
341
- # ToR).
342
- save_local_articles(Articles([article]))
343
- loaded += 1
344
- if (loaded % 10) == 0:
345
- self.post_message(
346
- self.SubTitle(f"Downloading articles from TheOldReader: {loaded}")
347
- )
348
- if loaded:
349
- self.notify(f"Articles downloaded: {loaded}")
350
- else:
351
- self.notify("No new articles found on TheOldReader")
352
- remember_we_last_grabbed_at(new_grab)
353
-
354
- async def _refresh_read_status(self) -> None:
355
- """Refresh the read status from the server."""
356
- self.post_message(
357
- self.SubTitle("Getting list of unread articles from TheOldReader")
358
- )
359
- remote_unread_articles = set(
360
- article_id.full_id
361
- for article_id in await ArticleIDs.load_unread(self._session)
362
- )
363
- self.post_message(self.SubTitle("Comparing against locally-read articles"))
364
- local_unread_articles = set(get_unread_article_ids())
365
- if mark_as_read := local_unread_articles - remote_unread_articles:
366
- locally_mark_article_ids_read(mark_as_read)
367
- self.notify(
368
- f"Articles found read elsewhere on TheOldReader: {len(mark_as_read)}"
369
- )
326
+ @on(SyncFinished)
327
+ def _sync_finished(self) -> None:
328
+ """Clean up after a sync from TheOldReader has finished."""
329
+ self._refresh_article_list()
330
+ self.post_message(self.SubTitle(""))
370
331
 
371
332
  @on(RefreshFromTheOldReader)
372
333
  @work(exclusive=True)
373
334
  async def action_refresh_from_the_old_reader_command(self) -> None:
374
335
  """Load the main data from TheOldReader."""
375
-
376
- # Get the folder list.
377
- self.post_message(self.SubTitle("Getting folder list"))
378
- self.post_message(
379
- self.NewFolders(save_local_folders(await Folders.load(self._session)))
380
- )
381
-
382
- # Get the subscriptions list.
383
- self.post_message(self.SubTitle("Getting subscriptions list"))
384
- self.post_message(
385
- self.NewSubscriptions(
386
- save_local_subscriptions(await Subscriptions.load(self._session))
387
- )
388
- )
389
-
390
- # Download the latest articles we don't know about.
391
- if never_grabbed_before := last_grabbed_data_at() is None:
392
- self.post_message(self.SubTitle("Getting available articles"))
393
- else:
394
- self.post_message(
395
- self.SubTitle(f"Getting articles new since {last_grabbed_data_at()}")
396
- )
397
- await self._download_newest_articles()
398
-
399
- # If we have grabbed data before, let's try and sync up what's been read.
400
- if not never_grabbed_before:
401
- await self._refresh_read_status()
402
-
403
- # Recalculate the unread counts.
404
- self.post_message(self.SubTitle("Calculating unread counts"))
405
- self.post_message(
406
- self.NewUnread(get_local_unread(self.folders, self.subscriptions))
407
- )
408
-
409
- # Finally we're all done.
410
- self.post_message(self.SubTitle(""))
336
+ await ToRSync(
337
+ self._session,
338
+ on_new_step=lambda step: self.post_message(self.SubTitle(step)),
339
+ on_new_result=lambda result: self.notify(result),
340
+ on_new_folders=lambda folders: self.post_message(self.NewFolders(folders)),
341
+ on_new_subscriptions=lambda subscriptions: self.post_message(
342
+ self.NewSubscriptions(subscriptions)
343
+ ),
344
+ on_new_unread=lambda unread: self.post_message(self.NewUnread(unread)),
345
+ on_sync_finished=lambda: self.post_message(self.SyncFinished()),
346
+ ).refresh()
411
347
 
412
348
  @on(Navigation.CategorySelected)
413
349
  def _handle_navigaion_selection(self, message: Navigation.CategorySelected) -> None:
@@ -418,14 +354,9 @@ class Main(EnhancedScreen[None]):
418
354
  """
419
355
  self.current_category = message.category
420
356
  self.article = None
421
- self.articles = get_local_articles(message.category, not self.show_all)
357
+ self._refresh_article_list()
422
358
  self.query_one(ArticleList).focus()
423
359
 
424
- def _refresh_article_list(self) -> None:
425
- """Refresh the content of the article list."""
426
- if category := self.query_one(Navigation).current_category:
427
- self.articles = get_local_articles(category, not self.show_all)
428
-
429
360
  def _watch_show_all(self) -> None:
430
361
  """Handle changes to the show all flag."""
431
362
  self._refresh_article_list()
@@ -0,0 +1,11 @@
1
+ """Code to sync the local data from TheOldReader."""
2
+
3
+ ##############################################################################
4
+ # Local imports.
5
+ from .sync import ToRSync
6
+
7
+ ##############################################################################
8
+ # Exports.
9
+ __all__ = ["ToRSync"]
10
+
11
+ ### __init__.py ends here
@@ -0,0 +1,158 @@
1
+ """Provides a class to sync data from TheOldReader."""
2
+
3
+ ##############################################################################
4
+ # Python imports.
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta, timezone
7
+ from typing import Any, Callable
8
+
9
+ ##############################################################################
10
+ # OldAS imports.
11
+ from oldas import ArticleIDs, Articles, Folders, Session, Subscriptions
12
+
13
+ ##############################################################################
14
+ from ..data import (
15
+ LocalUnread,
16
+ get_local_unread,
17
+ get_unread_article_ids,
18
+ last_grabbed_data_at,
19
+ load_configuration,
20
+ locally_mark_article_ids_read,
21
+ remember_we_last_grabbed_at,
22
+ save_local_articles,
23
+ save_local_folders,
24
+ save_local_subscriptions,
25
+ )
26
+
27
+ ##############################################################################
28
+ type Callback = Callable[[], Any] | None
29
+ """Type of a callback with no arguments."""
30
+ type CallbackWith[T] = Callable[[T], Any] | None
31
+ """Type of callback with a single argument."""
32
+
33
+
34
+ ##############################################################################
35
+ @dataclass
36
+ class ToRSync:
37
+ """Class that handles syncing data from TheOldReader."""
38
+
39
+ session: Session
40
+ """The TheOldReader API session object."""
41
+ on_new_step: CallbackWith[str] = None
42
+ """Function to call when a new step starts."""
43
+ on_new_result: CallbackWith[str] = None
44
+ """Function to call when a result should be communicated."""
45
+ on_new_folders: CallbackWith[Folders] = None
46
+ """Function to call when new folders are acquired."""
47
+ on_new_subscriptions: CallbackWith[Subscriptions] = None
48
+ """Function to call when new subscriptions are acquired."""
49
+ on_new_unread: CallbackWith[LocalUnread] = None
50
+ """Function to call when new unread counts are calculated."""
51
+ on_sync_finished: Callback = None
52
+ """Function to call when the sync has finished."""
53
+
54
+ def _step(self, step: str) -> None:
55
+ """Mark a new step.
56
+
57
+ Args:
58
+ step: The step that is happening.
59
+ """
60
+ if self.on_new_step:
61
+ self.on_new_step(step)
62
+
63
+ def _result(self, result: str) -> None:
64
+ """Show a new result.
65
+
66
+ Args:
67
+ result: The result that should be shown.
68
+ """
69
+ if self.on_new_result:
70
+ self.on_new_result(result)
71
+
72
+ async def _download_newest_articles(self) -> None:
73
+ """Download the latest articles available."""
74
+ last_grabbed = last_grabbed_data_at() or (
75
+ datetime.now() - timedelta(days=load_configuration().local_history)
76
+ )
77
+ new_grab = datetime.now(timezone.utc)
78
+ loaded = 0
79
+ async for article in Articles.stream_new_since(
80
+ self.session, last_grabbed, n=10
81
+ ):
82
+ # I've encountered articles that don't have an origin stream ID,
83
+ # which means that I can't relate them back to a stream, which
84
+ # means I'll never see them anyway...
85
+ if not article.origin.stream_id:
86
+ continue
87
+ # TODO: Right now I'm saving articles one at a time; perhaps I
88
+ # should save them in small batches? This would be simple enough
89
+ # -- perhaps same them in batches the same size as the buffer
90
+ # window I'm using right now (currently 10 articles per trip to
91
+ # ToR).
92
+ save_local_articles(Articles([article]))
93
+ loaded += 1
94
+ if (loaded % 10) == 0:
95
+ self._step(f"Downloading articles from TheOldReader: {loaded}")
96
+ if loaded:
97
+ self._result(f"Articles downloaded: {loaded}")
98
+ else:
99
+ self._result("No new articles found on TheOldReader")
100
+ remember_we_last_grabbed_at(new_grab)
101
+
102
+ async def _refresh_read_status(self) -> None:
103
+ """Refresh the read status from the server."""
104
+ self._step("Getting list of unread articles from TheOldReader")
105
+ remote_unread_articles = set(
106
+ article_id.full_id
107
+ for article_id in await ArticleIDs.load_unread(self.session)
108
+ )
109
+ self._step("Comparing against locally-read articles")
110
+ local_unread_articles = set(get_unread_article_ids())
111
+ if mark_as_read := local_unread_articles - remote_unread_articles:
112
+ locally_mark_article_ids_read(mark_as_read)
113
+ self._result(
114
+ f"Articles found read elsewhere on TheOldReader: {len(mark_as_read)}"
115
+ )
116
+
117
+ async def refresh(self) -> None:
118
+ """Refresh the data from TheOldReader.
119
+
120
+ Args:
121
+ session: The TheOldReader API session object.
122
+ """
123
+
124
+ # Get the folder list.
125
+ self._step("Getting folder list")
126
+ folders = save_local_folders(await Folders.load(self.session))
127
+ if self.on_new_folders:
128
+ self.on_new_folders(folders)
129
+
130
+ # Get the subscriptions list.
131
+ self._step("Getting subscriptions list")
132
+ subscriptions = save_local_subscriptions(await Subscriptions.load(self.session))
133
+ if self.on_new_subscriptions:
134
+ self.on_new_subscriptions(subscriptions)
135
+
136
+ # Download the latest articles we don't know about.
137
+ if never_grabbed_before := ((last_grab := last_grabbed_data_at()) is None):
138
+ self._step("Getting available articles")
139
+ else:
140
+ self._step(f"Getting new articles since {last_grab}")
141
+ await self._download_newest_articles()
142
+
143
+ # If we have grabbed data before, let's try and sync up what's been read.
144
+ if not never_grabbed_before:
145
+ await self._refresh_read_status()
146
+
147
+ # Recalculate the unread counts.
148
+ self._step("Calculating unread counts")
149
+ unread = get_local_unread(folders, subscriptions)
150
+ if self.on_new_unread:
151
+ self.on_new_unread(unread)
152
+
153
+ # Finally we're all done.
154
+ if self.on_sync_finished:
155
+ self.on_sync_finished()
156
+
157
+
158
+ ### sync.py ends here
@@ -17,7 +17,7 @@ from oldas import Article
17
17
  from textual.app import ComposeResult
18
18
  from textual.containers import Horizontal, Vertical, VerticalScroll
19
19
  from textual.reactive import var
20
- from textual.widgets import Label, Markdown
20
+ from textual.widgets import Label, Markdown, Rule
21
21
 
22
22
 
23
23
  ##############################################################################
@@ -32,6 +32,10 @@ class ArticleContent(Vertical):
32
32
  display: block;
33
33
  }
34
34
 
35
+ Rule.-horizontal {
36
+ margin: 0;
37
+ }
38
+
35
39
  #header {
36
40
  height: auto;
37
41
  padding: 0 1 0 1;
@@ -49,6 +53,15 @@ class ArticleContent(Vertical):
49
53
  }
50
54
  }
51
55
 
56
+ &:focus-within {
57
+ #header, Rule {
58
+ background: $boost;
59
+ }
60
+ Rule {
61
+ color: $border;
62
+ }
63
+ }
64
+
52
65
  Markdown {
53
66
  padding: 0 1 0 1;
54
67
  }
@@ -67,11 +80,13 @@ class ArticleContent(Vertical):
67
80
 
68
81
  def compose(self) -> ComposeResult:
69
82
  """Compose the content of the widget."""
83
+ yield Rule()
70
84
  with Vertical(id="header"):
71
85
  with Horizontal():
72
86
  yield Label(id="title", markup=False)
73
87
  yield Label(id="published")
74
88
  yield Label(id="link", markup=False)
89
+ yield Rule()
75
90
  with VerticalScroll():
76
91
  yield Markdown()
77
92
 
@@ -87,7 +102,7 @@ class ArticleContent(Vertical):
87
102
  link.visible = True
88
103
  link.update(self.article.html_url)
89
104
  await self.query_one(Markdown).update(convert(self.article.summary.content))
90
- self.query_one(VerticalScroll).scroll_home()
105
+ self.query_one(VerticalScroll).scroll_home(animate=False)
91
106
  self.set_class(self.article is not None, "--has-article")
92
107
 
93
108
  def focus(self, scroll_visible: bool = True) -> Self:
File without changes
File without changes
File without changes
File without changes
File without changes