aidoku/structs/
filter.rs

1use serde::{Deserialize, Serialize, ser::SerializeStruct};
2
3extern crate alloc;
4use alloc::{borrow::Cow, string::String, vec::Vec};
5
6/// A filter that can be used in a source search.
7///
8/// This struct shouldn't be constructed directly. Instead, use the individual
9/// filter structs and call [into](Into::into).
10#[derive(Debug, Clone, PartialEq)]
11pub struct Filter {
12	pub id: Cow<'static, str>,
13	pub title: Option<Cow<'static, str>>,
14	pub hide_from_header: Option<bool>,
15	pub kind: FilterKind,
16}
17/// A default value for a sort filter.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct SortFilterDefault {
20	pub index: i32,
21	pub ascending: bool,
22}
23
24/// The kind of filter.
25#[derive(Debug, Clone, PartialEq)]
26pub enum FilterKind {
27	/// A text field.
28	Text {
29		placeholder: Option<Cow<'static, str>>,
30	},
31	/// A list of sort options.
32	Sort {
33		can_ascend: bool,
34		options: Vec<Cow<'static, str>>,
35		default: Option<SortFilterDefault>,
36	},
37	/// A checkbox.
38	Check {
39		name: Option<Cow<'static, str>>,
40		can_exclude: bool,
41		default: Option<bool>,
42	},
43	/// A list of values that allows a single selection.
44	Select {
45		is_genre: bool,
46		uses_tag_style: bool,
47		options: Vec<Cow<'static, str>>,
48		ids: Option<Vec<Cow<'static, str>>>,
49		default: Option<Cow<'static, str>>,
50	},
51	/// A list of values that allows multiple selections.
52	MultiSelect {
53		is_genre: bool,
54		can_exclude: bool,
55		uses_tag_style: bool,
56		options: Vec<Cow<'static, str>>,
57		ids: Option<Vec<Cow<'static, str>>>,
58		default_included: Option<Vec<Cow<'static, str>>>,
59		default_excluded: Option<Vec<Cow<'static, str>>>,
60	},
61	/// A block of text displayed in the filter menu.
62	Note(Cow<'static, str>),
63	/// A range filter.
64	Range {
65		min: Option<f32>,
66		max: Option<f32>,
67		decimal: bool,
68	},
69}
70
71impl Filter {
72	/// Creates a new note filter with the given text.
73	pub fn note<A: Into<Cow<'static, str>>>(text: A) -> Self {
74		Self {
75			id: Cow::Borrowed("note"),
76			title: None,
77			hide_from_header: None,
78			kind: FilterKind::Note(text.into()),
79		}
80	}
81}
82
83macro_rules! create_filter_struct {
84	(
85		$struct_name:ident,
86		$filter_kind:ident,
87		$doc_comment:expr,
88		{ $($(#[$field_meta:meta])* $field_name:ident: $field_type:ty),* $(,)? },
89		{ $($def_field_name:ident: $default_value:expr),* $(,)? }
90	) => {
91		#[doc = $doc_comment]
92		///
93		/// This filter can be converted into a generic [Filter] using the [Into] trait.
94		#[derive(Debug, Clone, PartialEq)]
95		pub struct $struct_name {
96			/// The identifier for this filter.
97			pub id: Cow<'static, str>,
98			/// The display title for this filter.
99			pub title: Option<Cow<'static, str>>,
100			/// Whether this filter should be hidden from the filters list header.
101			pub hide_from_header: Option<bool>,
102			$(
103				$(#[$field_meta])*
104				pub $field_name: $field_type
105			),*
106		}
107
108		impl From<$struct_name> for Filter {
109			fn from(value: $struct_name) -> Filter {
110				Filter {
111					id: value.id,
112					title: value.title,
113					hide_from_header: value.hide_from_header,
114					kind: FilterKind::$filter_kind {
115						$($field_name: value.$field_name),*
116					},
117				}
118			}
119		}
120
121		impl Default for $struct_name {
122			fn default() -> Self {
123				Self {
124					id: Cow::Borrowed(stringify!($struct_name)),
125					title: None,
126					hide_from_header: None,
127					$($def_field_name: $default_value),*
128				}
129			}
130		}
131	};
132}
133
134create_filter_struct!(
135	TextFilter,
136	Text,
137	"A text field.",
138	{
139		/// Optional placeholder text to display when the field is empty.
140		placeholder: Option<Cow<'static, str>>,
141	},
142	{
143		placeholder: None,
144	}
145);
146
147create_filter_struct!(
148	SortFilter,
149	Sort,
150	"A list of sort options.",
151	{
152		/// Whether the sort can be ascending.
153		can_ascend: bool,
154		/// The list of available sort options.
155		options: Vec<Cow<'static, str>>,
156		/// The default sort option.
157		default: Option<SortFilterDefault>,
158	},
159	{
160		can_ascend: true,
161		options: Vec::new(),
162		default: None,
163	}
164);
165
166create_filter_struct!(
167	CheckFilter,
168	Check,
169	"A checkbox.",
170	{
171		/// Optional display name for the checkbox. If `None`, the title is used.
172		name: Option<Cow<'static, str>>,
173		/// Whether the checkbox can be excluded (tristate).
174		can_exclude: bool,
175		/// The default state of the checkbox.
176		default: Option<bool>,
177	},
178	{
179		name: None,
180		can_exclude: false,
181		default: None,
182	}
183);
184
185create_filter_struct!(
186	SelectFilter,
187	Select,
188	"A list of values that allows a single selection.",
189	{
190		/// Indicates if the filter is for genres.
191		is_genre: bool,
192		/// Whether to display the options as tags.
193		uses_tag_style: bool,
194		/// The list of options to display.
195		options: Vec<Cow<'static, str>>,
196		/// Optional IDs for each option. If not provided, the options are used.
197		ids: Option<Vec<Cow<'static, str>>>,
198		/// The default selected option.
199		default: Option<Cow<'static, str>>,
200	},
201	{
202		is_genre: false,
203		uses_tag_style: false,
204		options: Vec::new(),
205		ids: None,
206		default: None,
207	}
208);
209
210create_filter_struct!(
211	MultiSelectFilter,
212	MultiSelect,
213	"A list of values that allows multiple selections.",
214	{
215		/// Indicates if the filter is for genres.
216		is_genre: bool,
217		/// Whether options can be excluded as well as included.
218		can_exclude: bool,
219		/// Whether to display the options as tags.
220		uses_tag_style: bool,
221		/// The list of options to display.
222		options: Vec<Cow<'static, str>>,
223		/// Optional IDs for each option. If not provided, the options are used.
224		ids: Option<Vec<Cow<'static, str>>>,
225		default_included: Option<Vec<Cow<'static, str>>>,
226		default_excluded: Option<Vec<Cow<'static, str>>>,
227	},
228	{
229		is_genre: false,
230		can_exclude: false,
231		uses_tag_style: false,
232		options: Vec::new(),
233		ids: None,
234		default_included: None,
235		default_excluded: None,
236	}
237);
238
239create_filter_struct!(
240	RangeFilter,
241	Range,
242	"A range filter.",
243	{
244		/// The minimum value of the range.
245		min: Option<f32>,
246		/// The maximum value of the range.
247		max: Option<f32>,
248		/// Whether the range can be decimal.
249		decimal: bool,
250	},
251	{
252		min: None,
253		max: None,
254		decimal: false,
255	}
256);
257
258impl FilterKind {
259	fn raw_value(&self) -> &str {
260		match self {
261			FilterKind::Text { .. } => "text",
262			FilterKind::Sort { .. } => "sort",
263			FilterKind::Check { .. } => "check",
264			FilterKind::Select { .. } => "select",
265			FilterKind::MultiSelect { .. } => "multi-select",
266			FilterKind::Note(_) => "note",
267			FilterKind::Range { .. } => "range",
268		}
269	}
270}
271
272impl Serialize for Filter {
273	fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
274	where
275		S: serde::Serializer,
276	{
277		let mut state = serializer.serialize_struct("Filter", 0)?;
278		state.serialize_field("id", &Some(self.id.clone()))?;
279		state.serialize_field("title", &self.title)?;
280		state.serialize_field("hide_from_header", &self.hide_from_header)?;
281		state.serialize_field("type", &self.kind.raw_value())?;
282		match &self.kind {
283			FilterKind::Text { placeholder } => {
284				state.serialize_field("placeholder", &placeholder)?
285			}
286			FilterKind::Sort {
287				can_ascend,
288				options,
289				default,
290			} => {
291				state.serialize_field("can_ascend", &Some(can_ascend))?;
292				state.serialize_field("options", &options)?;
293				state.serialize_field("default", &default)?
294			}
295			FilterKind::Check {
296				name,
297				can_exclude,
298				default,
299			} => {
300				state.serialize_field("name", &name)?;
301				state.serialize_field("can_exclude", &Some(can_exclude))?;
302				state.serialize_field("default", &default)?
303			}
304			FilterKind::Select {
305				is_genre,
306				uses_tag_style,
307				options,
308				ids,
309				default,
310			} => {
311				state.serialize_field("is_genre", &Some(is_genre))?;
312				state.serialize_field("uses_tag_style", &Some(uses_tag_style))?;
313				state.serialize_field("options", &options)?;
314				state.serialize_field("ids", &ids)?;
315				state.serialize_field("default", &default)?
316			}
317			FilterKind::MultiSelect {
318				is_genre,
319				can_exclude,
320				uses_tag_style,
321				options,
322				ids,
323				default_included,
324				default_excluded,
325			} => {
326				state.serialize_field("is_genre", &Some(is_genre))?;
327				state.serialize_field("can_exclude", &Some(can_exclude))?;
328				state.serialize_field("uses_tag_style", &Some(uses_tag_style))?;
329				state.serialize_field("options", &options)?;
330				state.serialize_field("ids", &ids)?;
331				state.serialize_field("default_included", &default_included)?;
332				state.serialize_field("default_excluded", &default_excluded)?;
333			}
334			FilterKind::Note(text) => state.serialize_field("text", &text)?,
335			FilterKind::Range { min, max, decimal } => {
336				state.serialize_field("min", &min)?;
337				state.serialize_field("max", &max)?;
338				state.serialize_field("decimal", &Some(decimal))?;
339			}
340		};
341		state.end()
342	}
343}
344
345/// A configured filter value.
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub enum FilterValue {
348	/// A string from a text field.
349	Text {
350		/// The id of the filter.
351		id: String,
352		/// The value of the text field.
353		value: String,
354	},
355	/// A value from a sort filter.
356	Sort {
357		/// The id of the filter.
358		id: String,
359		/// The index of the sort option.
360		index: i32,
361		/// Whether the sort is ascending.
362		ascending: bool,
363	},
364	/// A value from a check filter.
365	Check {
366		/// The id of the filter.
367		id: String,
368		/// The value of the check filter.
369		value: i32,
370	},
371	/// A value from a select filter.
372	Select {
373		/// The id of the filter.
374		id: String,
375		/// The value of the select filter.
376		value: String,
377	},
378	/// A list of values from a multi-select filter.
379	MultiSelect {
380		/// The id of the filter.
381		id: String,
382		/// The list of included values.
383		included: Vec<String>,
384		/// The list of excluded values.
385		excluded: Vec<String>,
386	},
387	/// A range of values from a range filter.
388	Range {
389		/// The id of the filter.
390		id: String,
391		/// The starting value of the range.
392		from: Option<f32>,
393		/// The ending value of the range.
394		to: Option<f32>,
395	},
396}