Skip to content

Commit

Permalink
✨ Add raindrop types to the navigation panel
Browse files Browse the repository at this point in the history
  • Loading branch information
davep committed Jan 13, 2025
1 parent 1470fb0 commit 544512e
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 8 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
([#96](https://github.com/davep/braindrop/pull/96))
- Free text search now also looks in the domain of a raindrop.
([#96](https://github.com/davep/braindrop/pull/96))
- Added raindrop types to the navigation panel.

## v0.5.0

Expand Down
3 changes: 2 additions & 1 deletion src/braindrop/app/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)
from .exit_state import ExitState
from .local import LocalData, local_data_file
from .raindrops import Raindrops, TagCount
from .raindrops import Raindrops, TagCount, TypeCount
from .token import token_file

##############################################################################
Expand All @@ -23,6 +23,7 @@
"LocalData",
"Raindrops",
"TagCount",
"TypeCount",
"save_configuration",
"token_file",
"update_configuration",
Expand Down
74 changes: 74 additions & 0 deletions src/braindrop/app/data/raindrops.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
##############################################################################
# Python imports.
from dataclasses import dataclass
from functools import total_ordering
from typing import Callable, Counter, Iterable, Iterator

##############################################################################
Expand All @@ -18,6 +19,7 @@
from ...raindrop import (
Collection,
Raindrop,
RaindropType,
SpecialCollection,
Tag,
)
Expand Down Expand Up @@ -60,6 +62,28 @@ def _getter(data: TagCount) -> int:
return _getter


##############################################################################
@dataclass(frozen=True)
@total_ordering
class TypeCount:
"""Holds the count details of a raindrop type."""

type: RaindropType
"""The type."""
count: int
"""The count of raindrops of that type."""

def __gt__(self, value: object, /) -> bool:
if isinstance(value, TypeCount):
return self.type > value.type
raise NotImplementedError

def __eq__(self, value: object, /) -> bool:
if isinstance(value, TypeCount):
return self.type == value.type
raise NotImplementedError


##############################################################################
class Filter:
"""Base class for the raindrop filters."""
Expand Down Expand Up @@ -101,6 +125,29 @@ def __eq__(self, value: object) -> bool:
return str(value) == self._tag
return super().__eq__(value)

class IsOfType(Filter):
"""Filter class to check if a raindrop is of a given type."""

def __init__(self, raindrop_type: RaindropType) -> None:
"""Initialise the object.
Args:
raindrop_type: The raindrop type to filter on.
"""
self._type = raindrop_type
"""The type of raindrop to filter for."""

def __rand__(self, raindrop: Raindrop) -> bool:
return raindrop.type == self._type

def __str__(self) -> str:
return str(self._type)

def __eq__(self, value: object) -> bool:
if isinstance(value, Raindrops.IsOfType):
return str(value) == self._type
return super().__eq__(value)

class Containing(Filter):
"""Filter class to check if a raindrop contains some specific text."""

Expand Down Expand Up @@ -241,6 +288,12 @@ def unfiltered(self) -> Raindrops:
def description(self) -> str:
"""The description of the content of the Raindrop grouping."""
filters = []
if raindrop_types := [
f"{raindrop_type}"
for raindrop_type in self._filters
if isinstance(raindrop_type, self.IsOfType)
]:
filters.append(f"type {' and '.join(raindrop_types)}")
if search_text := [
f'"{text}"' for text in self._filters if isinstance(text, self.Containing)
]:
Expand All @@ -257,6 +310,16 @@ def tags(self) -> list[TagCount]:
tags.extend(set(raindrop.tags))
return [TagCount(name, count) for name, count in Counter(tags).items()]

@property
def types(self) -> list[TypeCount]:
"""The list of types found amongst the Raindrops."""
return [
TypeCount(name, count)
for name, count in Counter[RaindropType](
raindrop.type for raindrop in self
).items()
]

def __and__(self, new_filter: Filter) -> Raindrops:
"""Get the raindrops that match a given filter.
Expand Down Expand Up @@ -289,6 +352,17 @@ def tagged(self, tag: Tag | str) -> Raindrops:
"""
return self & self.Tagged(tag)

def of_type(self, raindrop_type: RaindropType) -> Raindrops:
"""Get the raindrops of a given type.
Args:
raindrop_type: The type to look for.
Returns:
The subset of Raindrops that are of the type.
"""
return self & self.IsOfType(raindrop_type)

def containing(self, search_text: str) -> Raindrops:
"""Get the raindrops containing the given text.
Expand Down
3 changes: 2 additions & 1 deletion src/braindrop/app/messages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

##############################################################################
# Local imports.
from .main import ShowCollection, ShowTagged
from .main import ShowCollection, ShowOfType, ShowTagged

##############################################################################
# Exports.
__all__ = [
"ShowCollection",
"ShowOfType",
"ShowTagged",
]

Expand Down
11 changes: 10 additions & 1 deletion src/braindrop/app/messages/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

##############################################################################
# Local imports.
from ...raindrop import Collection, Tag
from ...raindrop import Collection, RaindropType, Tag


##############################################################################
Expand All @@ -22,6 +22,15 @@ class ShowCollection(Message):
"""The collection to show."""


##############################################################################
@dataclass
class ShowOfType(Message):
"""A message that requests that Raindrops of a particular type are shown."""

raindrop_type: RaindropType
"""The raindrop type to filter on."""


##############################################################################
@dataclass
class ShowTagged(Message):
Expand Down
11 changes: 10 additions & 1 deletion src/braindrop/app/screens/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
token_file,
update_configuration,
)
from ..messages import ShowCollection, ShowTagged
from ..messages import ShowCollection, ShowOfType, ShowTagged
from ..providers import CollectionCommands, CommandsProvider, MainCommands, TagCommands
from ..widgets import Navigation, RaindropDetails, RaindropsView
from .confirm import Confirm
Expand Down Expand Up @@ -350,6 +350,15 @@ def action_search_tags_command(self) -> None:
severity="information",
)

@on(ShowOfType)
def command_show_of_type(self, command: ShowOfType) -> None:
"""handle the command that requests we show Raindrops of a given type.
Args:
command: The command.
"""
self.active_collection = self.active_collection.of_type(command.raindrop_type)

@on(ShowTagged)
def command_show_tagged(self, command: ShowTagged) -> None:
"""Handle the command that requests we show Raindrops with a given tag.
Expand Down
61 changes: 57 additions & 4 deletions src/braindrop/app/widgets/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
from textual.widgets import OptionList
from textual.widgets.option_list import Option

from braindrop.raindrop.raindrop import RaindropType

##############################################################################
# Local imports.
from ...raindrop import API, Collection, SpecialCollection, Tag
from ..commands import ShowAll, ShowUnsorted, ShowUntagged
from ..data import LocalData, Raindrops, TagCount
from ..messages import ShowCollection, ShowTagged
from ..data import LocalData, Raindrops, TagCount, TypeCount
from ..messages import ShowCollection, ShowOfType, ShowTagged
from .extended_option_list import OptionListEx


Expand Down Expand Up @@ -98,6 +100,41 @@ def prompt(self) -> RenderableType:
return prompt


##############################################################################
class TypeView(Option):
"""Option for showing a raindrop type."""

def __init__(self, raindrop_type: TypeCount) -> None:
"""Initialise the object.
Args:
raindrop_type: The type to show.
"""
self._type = raindrop_type
"""The type being viewed."""
super().__init__(self.prompt, id=f"_type_{self._type.type}")

@property
def prompt(self) -> RenderableType:
"""The prompt for the type.
Returns:
A renderable that is the prompt.
"""
prompt = Table.grid(expand=True)
prompt.add_column(ratio=1)
prompt.add_column(justify="right")
prompt.add_row(
str(self._type.type.capitalize()), f"[dim i]{self._type.count}[/]"
)
return prompt

@property
def type(self) -> RaindropType:
"""The raindrop type."""
return self._type.type


##############################################################################
class TagView(Option):
"""Option for showing a tag."""
Expand Down Expand Up @@ -329,14 +366,25 @@ def _by_count(tags: list[TagCount]) -> list[TagCount]:
"""
return sorted(tags, key=TagCount.the_count(), reverse=True)

def _show_types_for(self, collection: Raindrops) -> None:
"""Show types relating to a given collection.
Args:
collection: The collection to show the types for.
"""
with self.preserved_highlight:
if self.data is not None and (types := collection.types):
self.add_option(Title(f"Types ({len(types)})"))
for raindrop_type in sorted(types):
self.add_option(TypeView(raindrop_type))

def _show_tags_for(self, collection: Raindrops) -> None:
"""Show tags relating a given collection.
Args:
collection: The collection to show the tags for.
"""
with self.preserved_highlight:
self._main_navigation()
if self.data is not None and (tags := collection.tags):
self.add_option(Title(f"Tags ({len(tags)})"))
for tag in (self._by_count if self.tags_by_count else self._by_name)(
Expand All @@ -351,7 +399,10 @@ def watch_data(self) -> None:

def watch_active_collection(self) -> None:
"""React to the currently-active collection being changed."""
self._show_tags_for(self.active_collection)
with self.preserved_highlight:
self._main_navigation()
self._show_types_for(self.active_collection)
self._show_tags_for(self.active_collection)
self._refresh_lines() # https://github.com/Textualize/textual/issues/5431

def watch_tags_by_count(self) -> None:
Expand All @@ -370,6 +421,8 @@ def _collection_selected(self, message: OptionList.OptionSelected) -> None:
self.post_message(ShowCollection(message.option.collection))
elif isinstance(message.option, TagView):
self.post_message(ShowTagged(message.option.tag))
elif isinstance(message.option, TypeView):
self.post_message(ShowOfType(message.option.type))


### navigation.py ends here

0 comments on commit 544512e

Please sign in to comment.