schenesort 2.2.0__py3-none-any.whl → 2.4.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.
- schenesort/cli.py +253 -0
- schenesort/thumbnails.py +127 -0
- schenesort/tui/__init__.py +11 -1
- schenesort/tui/grid_app.py +302 -0
- schenesort/tui/widgets/filter_panel.py +249 -0
- schenesort/tui/widgets/thumbnail_grid.py +291 -0
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/METADATA +116 -13
- schenesort-2.4.0.dist-info/RECORD +19 -0
- schenesort-2.2.0.dist-info/RECORD +0 -15
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/WHEEL +0 -0
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/entry_points.txt +0 -0
- {schenesort-2.2.0.dist-info → schenesort-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Gallery Grid Browser TUI application."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual.app import App, ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import Horizontal
|
|
8
|
+
from textual.screen import Screen
|
|
9
|
+
from textual.widgets import Footer, Header, Static
|
|
10
|
+
|
|
11
|
+
from schenesort.db import WallpaperDB
|
|
12
|
+
from schenesort.tui.widgets.filter_panel import FilterPanel, FilterValues
|
|
13
|
+
from schenesort.tui.widgets.image_preview import ImagePreview
|
|
14
|
+
from schenesort.tui.widgets.metadata_panel import MetadataPanel
|
|
15
|
+
from schenesort.tui.widgets.thumbnail_grid import ThumbnailGrid
|
|
16
|
+
from schenesort.xmp import read_xmp
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DetailScreen(Screen):
|
|
20
|
+
"""Screen for viewing a single image with metadata."""
|
|
21
|
+
|
|
22
|
+
BINDINGS = [
|
|
23
|
+
Binding("escape", "pop_screen", "Back", show=True),
|
|
24
|
+
Binding("q", "pop_screen", "Back", show=False),
|
|
25
|
+
Binding("j", "next_image", "Next", show=True),
|
|
26
|
+
Binding("k", "prev_image", "Previous", show=True),
|
|
27
|
+
Binding("down", "next_image", "Next", show=False),
|
|
28
|
+
Binding("up", "prev_image", "Previous", show=False),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
CSS = """
|
|
32
|
+
DetailScreen {
|
|
33
|
+
layout: vertical;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
DetailScreen #detail-container {
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: 1fr;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
DetailScreen #image-panel {
|
|
42
|
+
width: 60%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
border: solid $primary;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
DetailScreen #metadata-panel {
|
|
48
|
+
width: 40%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
border: solid $secondary;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
DetailScreen #detail-status {
|
|
54
|
+
dock: bottom;
|
|
55
|
+
height: 1;
|
|
56
|
+
background: $surface;
|
|
57
|
+
padding: 0 1;
|
|
58
|
+
}
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, images: list[Path], start_index: int = 0, **kwargs) -> None:
|
|
62
|
+
super().__init__(**kwargs)
|
|
63
|
+
self._images = images
|
|
64
|
+
self._current_index = start_index
|
|
65
|
+
|
|
66
|
+
def compose(self) -> ComposeResult:
|
|
67
|
+
yield Header()
|
|
68
|
+
with Horizontal(id="detail-container"):
|
|
69
|
+
yield ImagePreview(id="image-panel")
|
|
70
|
+
yield MetadataPanel(id="metadata-panel")
|
|
71
|
+
yield Static("", id="detail-status")
|
|
72
|
+
yield Footer()
|
|
73
|
+
|
|
74
|
+
def on_mount(self) -> None:
|
|
75
|
+
"""Show the initial image."""
|
|
76
|
+
self._show_current_image()
|
|
77
|
+
|
|
78
|
+
def _show_current_image(self) -> None:
|
|
79
|
+
"""Display the current image and metadata."""
|
|
80
|
+
if not self._images or not (0 <= self._current_index < len(self._images)):
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
current = self._images[self._current_index]
|
|
84
|
+
|
|
85
|
+
# Update image
|
|
86
|
+
preview = self.query_one("#image-panel", ImagePreview)
|
|
87
|
+
preview.load_image(current)
|
|
88
|
+
|
|
89
|
+
# Update metadata
|
|
90
|
+
metadata = read_xmp(current)
|
|
91
|
+
panel = self.query_one("#metadata-panel", MetadataPanel)
|
|
92
|
+
panel.update_metadata(metadata, current.name)
|
|
93
|
+
|
|
94
|
+
# Update status
|
|
95
|
+
status = self.query_one("#detail-status", Static)
|
|
96
|
+
status.update(f"{current.name} [{self._current_index + 1}/{len(self._images)}]")
|
|
97
|
+
|
|
98
|
+
def action_next_image(self) -> None:
|
|
99
|
+
"""Show next image."""
|
|
100
|
+
if self._current_index < len(self._images) - 1:
|
|
101
|
+
self._current_index += 1
|
|
102
|
+
self._show_current_image()
|
|
103
|
+
|
|
104
|
+
def action_prev_image(self) -> None:
|
|
105
|
+
"""Show previous image."""
|
|
106
|
+
if self._current_index > 0:
|
|
107
|
+
self._current_index -= 1
|
|
108
|
+
self._show_current_image()
|
|
109
|
+
|
|
110
|
+
def action_pop_screen(self) -> None:
|
|
111
|
+
"""Return to the grid view."""
|
|
112
|
+
self.app.pop_screen()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class GridBrowser(App):
|
|
116
|
+
"""A TUI application for browsing wallpapers in a thumbnail grid with filters."""
|
|
117
|
+
|
|
118
|
+
TITLE = "Schenesort - Gallery"
|
|
119
|
+
|
|
120
|
+
CSS = """
|
|
121
|
+
Screen {
|
|
122
|
+
layout: vertical;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#main-container {
|
|
126
|
+
width: 100%;
|
|
127
|
+
height: 1fr;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#filter-panel {
|
|
131
|
+
width: 22;
|
|
132
|
+
height: 100%;
|
|
133
|
+
border: solid $secondary;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#grid-panel {
|
|
137
|
+
width: 1fr;
|
|
138
|
+
height: 100%;
|
|
139
|
+
border: solid $primary;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#status-bar {
|
|
143
|
+
dock: bottom;
|
|
144
|
+
height: 1;
|
|
145
|
+
background: $surface;
|
|
146
|
+
color: $text;
|
|
147
|
+
padding: 0 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#status-left {
|
|
151
|
+
width: 1fr;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#status-right {
|
|
155
|
+
width: auto;
|
|
156
|
+
}
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
BINDINGS = [
|
|
160
|
+
Binding("q", "quit", "Quit", show=True),
|
|
161
|
+
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
|
|
162
|
+
Binding("ctrl+q", "quit", "Quit", show=False, priority=True),
|
|
163
|
+
Binding("tab", "focus_next_panel", "Switch Panel", show=True),
|
|
164
|
+
Binding("shift+tab", "focus_prev_panel", "Switch Panel", show=False),
|
|
165
|
+
Binding("enter", "open_detail", "Open", show=True),
|
|
166
|
+
Binding("escape", "clear_filters", "Clear Filters", show=True),
|
|
167
|
+
Binding("r", "refresh", "Refresh", show=True),
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
def __init__(self, initial_filters: FilterValues | None = None, **kwargs) -> None:
|
|
171
|
+
super().__init__(**kwargs)
|
|
172
|
+
self._initial_filters = initial_filters or FilterValues()
|
|
173
|
+
self._images: list[Path] = []
|
|
174
|
+
self._db_path: Path | None = None
|
|
175
|
+
|
|
176
|
+
def compose(self) -> ComposeResult:
|
|
177
|
+
yield Header()
|
|
178
|
+
with Horizontal(id="main-container"):
|
|
179
|
+
yield FilterPanel(initial_filters=self._initial_filters, id="filter-panel")
|
|
180
|
+
yield ThumbnailGrid(id="grid-panel")
|
|
181
|
+
with Horizontal(id="status-bar"):
|
|
182
|
+
yield Static("Loading...", id="status-left")
|
|
183
|
+
yield Static("", id="status-right")
|
|
184
|
+
yield Footer()
|
|
185
|
+
|
|
186
|
+
def on_mount(self) -> None:
|
|
187
|
+
"""Initialize the browser when mounted."""
|
|
188
|
+
# Set initial focus to the grid
|
|
189
|
+
self.query_one("#grid-panel", ThumbnailGrid).focus()
|
|
190
|
+
# Load initial data
|
|
191
|
+
self._query_database(self._initial_filters)
|
|
192
|
+
|
|
193
|
+
def _query_database(self, filters: FilterValues) -> None:
|
|
194
|
+
"""Query the database with the given filters."""
|
|
195
|
+
try:
|
|
196
|
+
with WallpaperDB() as db:
|
|
197
|
+
self._db_path = db.db_path
|
|
198
|
+
results = db.query(
|
|
199
|
+
search=filters.search or None,
|
|
200
|
+
tag=filters.tag or None,
|
|
201
|
+
mood=filters.mood or None,
|
|
202
|
+
color=filters.color or None,
|
|
203
|
+
style=filters.style or None,
|
|
204
|
+
subject=filters.subject or None,
|
|
205
|
+
time_of_day=filters.time or None,
|
|
206
|
+
screen=filters.screen or None,
|
|
207
|
+
min_width=filters.min_width,
|
|
208
|
+
min_height=filters.min_height,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Extract paths and filter for existing files
|
|
212
|
+
self._images = [Path(r["path"]) for r in results if Path(r["path"]).exists()]
|
|
213
|
+
|
|
214
|
+
# Update grid
|
|
215
|
+
grid = self.query_one("#grid-panel", ThumbnailGrid)
|
|
216
|
+
grid.set_images(self._images)
|
|
217
|
+
|
|
218
|
+
# Update status
|
|
219
|
+
self._update_status()
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
self._update_status(error=str(e))
|
|
223
|
+
|
|
224
|
+
def _update_status(self, error: str | None = None) -> None:
|
|
225
|
+
"""Update the status bar."""
|
|
226
|
+
left_status = self.query_one("#status-left", Static)
|
|
227
|
+
right_status = self.query_one("#status-right", Static)
|
|
228
|
+
|
|
229
|
+
if error:
|
|
230
|
+
left_status.update(f"[red]Error: {error}[/red]")
|
|
231
|
+
right_status.update("")
|
|
232
|
+
else:
|
|
233
|
+
count = len(self._images)
|
|
234
|
+
if count == 0:
|
|
235
|
+
left_status.update("[dim]No images matching filters[/dim]")
|
|
236
|
+
else:
|
|
237
|
+
left_status.update(f"{count} image{'s' if count != 1 else ''} matching")
|
|
238
|
+
|
|
239
|
+
grid = self.query_one("#grid-panel", ThumbnailGrid)
|
|
240
|
+
if grid.image_count > 0:
|
|
241
|
+
right_status.update(f"{grid.selected_index + 1}/{grid.image_count}")
|
|
242
|
+
else:
|
|
243
|
+
right_status.update("")
|
|
244
|
+
|
|
245
|
+
def on_filter_panel_filters_changed(self, event: FilterPanel.FiltersChanged) -> None:
|
|
246
|
+
"""Handle filter changes from the filter panel."""
|
|
247
|
+
self._query_database(event.filters)
|
|
248
|
+
|
|
249
|
+
def on_thumbnail_grid_selection_changed(
|
|
250
|
+
self,
|
|
251
|
+
event: ThumbnailGrid.SelectionChanged, # noqa: ARG002
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Handle selection changes in the grid."""
|
|
254
|
+
self._update_status()
|
|
255
|
+
|
|
256
|
+
def on_thumbnail_grid_image_selected(self, event: ThumbnailGrid.ImageSelected) -> None:
|
|
257
|
+
"""Handle image selection (Enter pressed) - open detail view."""
|
|
258
|
+
self._open_detail_view(event.index)
|
|
259
|
+
|
|
260
|
+
def _open_detail_view(self, start_index: int = 0) -> None:
|
|
261
|
+
"""Open the detail view with the current image list."""
|
|
262
|
+
if not self._images:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Push detail screen onto the stack
|
|
266
|
+
self.push_screen(DetailScreen(self._images, start_index))
|
|
267
|
+
|
|
268
|
+
def action_focus_next_panel(self) -> None:
|
|
269
|
+
"""Switch focus to the next panel."""
|
|
270
|
+
current = self.focused
|
|
271
|
+
filter_panel = self.query_one("#filter-panel", FilterPanel)
|
|
272
|
+
grid_panel = self.query_one("#grid-panel", ThumbnailGrid)
|
|
273
|
+
|
|
274
|
+
# Check if focus is in the filter panel area
|
|
275
|
+
if current is not None and filter_panel in current.ancestors_with_self:
|
|
276
|
+
grid_panel.focus()
|
|
277
|
+
else:
|
|
278
|
+
# Focus the first input in the filter panel
|
|
279
|
+
try:
|
|
280
|
+
first_input = filter_panel.query("Input").first()
|
|
281
|
+
first_input.focus()
|
|
282
|
+
except Exception:
|
|
283
|
+
filter_panel.focus()
|
|
284
|
+
|
|
285
|
+
def action_focus_prev_panel(self) -> None:
|
|
286
|
+
"""Switch focus to the previous panel (reverse of next)."""
|
|
287
|
+
self.action_focus_next_panel()
|
|
288
|
+
|
|
289
|
+
def action_open_detail(self) -> None:
|
|
290
|
+
"""Open detail view for the selected image."""
|
|
291
|
+
grid = self.query_one("#grid-panel", ThumbnailGrid)
|
|
292
|
+
self._open_detail_view(grid.selected_index)
|
|
293
|
+
|
|
294
|
+
def action_clear_filters(self) -> None:
|
|
295
|
+
"""Clear all filters."""
|
|
296
|
+
filter_panel = self.query_one("#filter-panel", FilterPanel)
|
|
297
|
+
filter_panel.clear_filters()
|
|
298
|
+
|
|
299
|
+
def action_refresh(self) -> None:
|
|
300
|
+
"""Refresh the grid with current filters."""
|
|
301
|
+
filter_panel = self.query_one("#filter-panel", FilterPanel)
|
|
302
|
+
self._query_database(filter_panel.filters)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Filter panel widget for gallery grid view."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import VerticalScroll
|
|
7
|
+
from textual.message import Message
|
|
8
|
+
from textual.widgets import Input, Label, Static
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class FilterValues:
|
|
13
|
+
"""Container for all filter values."""
|
|
14
|
+
|
|
15
|
+
search: str = ""
|
|
16
|
+
tag: str = ""
|
|
17
|
+
mood: str = ""
|
|
18
|
+
color: str = ""
|
|
19
|
+
style: str = ""
|
|
20
|
+
subject: str = ""
|
|
21
|
+
time: str = ""
|
|
22
|
+
screen: str = ""
|
|
23
|
+
min_width: int | None = None
|
|
24
|
+
min_height: int | None = None
|
|
25
|
+
|
|
26
|
+
def is_empty(self) -> bool:
|
|
27
|
+
"""Check if all filters are empty."""
|
|
28
|
+
return (
|
|
29
|
+
not self.search
|
|
30
|
+
and not self.tag
|
|
31
|
+
and not self.mood
|
|
32
|
+
and not self.color
|
|
33
|
+
and not self.style
|
|
34
|
+
and not self.subject
|
|
35
|
+
and not self.time
|
|
36
|
+
and not self.screen
|
|
37
|
+
and self.min_width is None
|
|
38
|
+
and self.min_height is None
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FilterPanel(VerticalScroll):
|
|
43
|
+
"""Sidebar panel with filter inputs for gallery view."""
|
|
44
|
+
|
|
45
|
+
class FiltersChanged(Message):
|
|
46
|
+
"""Emitted when any filter value changes."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, filters: FilterValues) -> None:
|
|
49
|
+
self.filters = filters
|
|
50
|
+
super().__init__()
|
|
51
|
+
|
|
52
|
+
DEFAULT_CSS = """
|
|
53
|
+
FilterPanel {
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
background: $surface;
|
|
57
|
+
padding: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
FilterPanel .filter-title {
|
|
61
|
+
text-style: bold;
|
|
62
|
+
color: $text;
|
|
63
|
+
margin-bottom: 1;
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
FilterPanel .filter-label {
|
|
68
|
+
color: $text-muted;
|
|
69
|
+
margin-top: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
FilterPanel Input {
|
|
73
|
+
margin-bottom: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
FilterPanel Input:focus {
|
|
77
|
+
border: tall $accent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
FilterPanel .filter-section {
|
|
81
|
+
margin-top: 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
FilterPanel .filter-hint {
|
|
85
|
+
color: $text-disabled;
|
|
86
|
+
text-style: italic;
|
|
87
|
+
margin-top: 1;
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, initial_filters: FilterValues | None = None, **kwargs) -> None:
|
|
92
|
+
super().__init__(**kwargs)
|
|
93
|
+
self._filters = initial_filters or FilterValues()
|
|
94
|
+
self._pending_change: int = 0
|
|
95
|
+
|
|
96
|
+
def compose(self) -> ComposeResult:
|
|
97
|
+
yield Static("Filters", classes="filter-title")
|
|
98
|
+
|
|
99
|
+
yield Label("Search", classes="filter-label")
|
|
100
|
+
yield Input(
|
|
101
|
+
value=self._filters.search,
|
|
102
|
+
placeholder="description, scene...",
|
|
103
|
+
id="filter-search",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
yield Label("Tag", classes="filter-label")
|
|
107
|
+
yield Input(
|
|
108
|
+
value=self._filters.tag,
|
|
109
|
+
placeholder="e.g. nature",
|
|
110
|
+
id="filter-tag",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
yield Label("Mood", classes="filter-label")
|
|
114
|
+
yield Input(
|
|
115
|
+
value=self._filters.mood,
|
|
116
|
+
placeholder="e.g. peaceful",
|
|
117
|
+
id="filter-mood",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
yield Label("Color", classes="filter-label")
|
|
121
|
+
yield Input(
|
|
122
|
+
value=self._filters.color,
|
|
123
|
+
placeholder="e.g. blue",
|
|
124
|
+
id="filter-color",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
yield Label("Style", classes="filter-label")
|
|
128
|
+
yield Input(
|
|
129
|
+
value=self._filters.style,
|
|
130
|
+
placeholder="e.g. photography",
|
|
131
|
+
id="filter-style",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
yield Label("Subject", classes="filter-label")
|
|
135
|
+
yield Input(
|
|
136
|
+
value=self._filters.subject,
|
|
137
|
+
placeholder="e.g. landscape",
|
|
138
|
+
id="filter-subject",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
yield Label("Time", classes="filter-label")
|
|
142
|
+
yield Input(
|
|
143
|
+
value=self._filters.time,
|
|
144
|
+
placeholder="e.g. sunset",
|
|
145
|
+
id="filter-time",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
yield Label("Screen", classes="filter-label")
|
|
149
|
+
yield Input(
|
|
150
|
+
value=self._filters.screen,
|
|
151
|
+
placeholder="e.g. 4K, 1440p",
|
|
152
|
+
id="filter-screen",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
yield Label("Min Width", classes="filter-label")
|
|
156
|
+
yield Input(
|
|
157
|
+
value=str(self._filters.min_width) if self._filters.min_width else "",
|
|
158
|
+
placeholder="pixels",
|
|
159
|
+
id="filter-min-width",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
yield Label("Min Height", classes="filter-label")
|
|
163
|
+
yield Input(
|
|
164
|
+
value=str(self._filters.min_height) if self._filters.min_height else "",
|
|
165
|
+
placeholder="pixels",
|
|
166
|
+
id="filter-min-height",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
yield Static("Press Tab to switch to grid", classes="filter-hint")
|
|
170
|
+
|
|
171
|
+
def on_input_changed(self, event: Input.Changed) -> None: # noqa: ARG002
|
|
172
|
+
"""Handle input changes with debouncing."""
|
|
173
|
+
# Increment counter to track which change this is
|
|
174
|
+
self._pending_change += 1
|
|
175
|
+
current_change = self._pending_change
|
|
176
|
+
|
|
177
|
+
# Schedule the filter update with a small delay
|
|
178
|
+
self.set_timer(0.3, lambda: self._emit_filter_change(current_change))
|
|
179
|
+
|
|
180
|
+
def _emit_filter_change(self, change_id: int) -> None:
|
|
181
|
+
"""Gather all filter values and emit the change message."""
|
|
182
|
+
# Only process if this is the most recent change
|
|
183
|
+
if change_id != self._pending_change:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# Read all input values
|
|
187
|
+
search = self.query_one("#filter-search", Input).value.strip()
|
|
188
|
+
tag = self.query_one("#filter-tag", Input).value.strip()
|
|
189
|
+
mood = self.query_one("#filter-mood", Input).value.strip()
|
|
190
|
+
color = self.query_one("#filter-color", Input).value.strip()
|
|
191
|
+
style = self.query_one("#filter-style", Input).value.strip()
|
|
192
|
+
subject = self.query_one("#filter-subject", Input).value.strip()
|
|
193
|
+
time = self.query_one("#filter-time", Input).value.strip()
|
|
194
|
+
screen = self.query_one("#filter-screen", Input).value.strip()
|
|
195
|
+
|
|
196
|
+
# Parse numeric values
|
|
197
|
+
min_width_str = self.query_one("#filter-min-width", Input).value.strip()
|
|
198
|
+
min_height_str = self.query_one("#filter-min-height", Input).value.strip()
|
|
199
|
+
|
|
200
|
+
min_width = None
|
|
201
|
+
min_height = None
|
|
202
|
+
try:
|
|
203
|
+
if min_width_str:
|
|
204
|
+
min_width = int(min_width_str)
|
|
205
|
+
except ValueError:
|
|
206
|
+
pass
|
|
207
|
+
try:
|
|
208
|
+
if min_height_str:
|
|
209
|
+
min_height = int(min_height_str)
|
|
210
|
+
except ValueError:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
self._filters = FilterValues(
|
|
214
|
+
search=search,
|
|
215
|
+
tag=tag,
|
|
216
|
+
mood=mood,
|
|
217
|
+
color=color,
|
|
218
|
+
style=style,
|
|
219
|
+
subject=subject,
|
|
220
|
+
time=time,
|
|
221
|
+
screen=screen,
|
|
222
|
+
min_width=min_width,
|
|
223
|
+
min_height=min_height,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
self.post_message(self.FiltersChanged(self._filters))
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def filters(self) -> FilterValues:
|
|
230
|
+
"""Get the current filter values."""
|
|
231
|
+
return self._filters
|
|
232
|
+
|
|
233
|
+
def clear_filters(self) -> None:
|
|
234
|
+
"""Clear all filter inputs."""
|
|
235
|
+
for input_id in [
|
|
236
|
+
"#filter-search",
|
|
237
|
+
"#filter-tag",
|
|
238
|
+
"#filter-mood",
|
|
239
|
+
"#filter-color",
|
|
240
|
+
"#filter-style",
|
|
241
|
+
"#filter-subject",
|
|
242
|
+
"#filter-time",
|
|
243
|
+
"#filter-screen",
|
|
244
|
+
"#filter-min-width",
|
|
245
|
+
"#filter-min-height",
|
|
246
|
+
]:
|
|
247
|
+
self.query_one(input_id, Input).value = ""
|
|
248
|
+
self._filters = FilterValues()
|
|
249
|
+
self.post_message(self.FiltersChanged(self._filters))
|