Initial Comicviewer implementation
This commit is contained in:
commit
cb01395612
20 changed files with 4940 additions and 0 deletions
181
src/browser.rs
Normal file
181
src/browser.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EntryKind {
|
||||
Directory,
|
||||
ZipArchive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrowserEntry {
|
||||
pub uri: String,
|
||||
pub name: String,
|
||||
pub kind: EntryKind,
|
||||
pub size: Option<u64>,
|
||||
pub modified: Option<SystemTime>,
|
||||
pub created: Option<SystemTime>,
|
||||
pub etag: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SortField {
|
||||
Name,
|
||||
Modified,
|
||||
Created,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SortDirection {
|
||||
Ascending,
|
||||
Descending,
|
||||
}
|
||||
|
||||
pub fn sort_entries(entries: &mut [BrowserEntry], field: SortField, direction: SortDirection) {
|
||||
entries.sort_by(|left, right| {
|
||||
let kind_order = match (left.kind, right.kind) {
|
||||
(EntryKind::Directory, EntryKind::ZipArchive) => Ordering::Less,
|
||||
(EntryKind::ZipArchive, EntryKind::Directory) => Ordering::Greater,
|
||||
_ => Ordering::Equal,
|
||||
};
|
||||
if kind_order != Ordering::Equal {
|
||||
return kind_order;
|
||||
}
|
||||
|
||||
match field {
|
||||
SortField::Name => apply_direction(natural_cmp(&left.name, &right.name), direction),
|
||||
SortField::Modified => compare_optional_time(left.modified, right.modified, direction)
|
||||
.then_with(|| apply_direction(natural_cmp(&left.name, &right.name), direction)),
|
||||
SortField::Created => compare_optional_time(left.created, right.created, direction)
|
||||
.then_with(|| apply_direction(natural_cmp(&left.name, &right.name), direction)),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn compare_optional_time(
|
||||
left: Option<SystemTime>,
|
||||
right: Option<SystemTime>,
|
||||
direction: SortDirection,
|
||||
) -> Ordering {
|
||||
match (left, right) {
|
||||
(Some(left), Some(right)) => apply_direction(left.cmp(&right), direction),
|
||||
(Some(_), None) => Ordering::Less,
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(None, None) => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_direction(order: Ordering, direction: SortDirection) -> Ordering {
|
||||
match direction {
|
||||
SortDirection::Ascending => order,
|
||||
SortDirection::Descending => order.reverse(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zip_path(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("zip"))
|
||||
}
|
||||
|
||||
pub fn natural_cmp(left: &str, right: &str) -> Ordering {
|
||||
let mut left_chars = left.chars().peekable();
|
||||
let mut right_chars = right.chars().peekable();
|
||||
|
||||
loop {
|
||||
match (left_chars.peek(), right_chars.peek()) {
|
||||
(None, None) => return Ordering::Equal,
|
||||
(None, Some(_)) => return Ordering::Less,
|
||||
(Some(_), None) => return Ordering::Greater,
|
||||
(Some(left_char), Some(right_char))
|
||||
if left_char.is_ascii_digit() && right_char.is_ascii_digit() =>
|
||||
{
|
||||
let left_number = take_digits(&mut left_chars);
|
||||
let right_number = take_digits(&mut right_chars);
|
||||
let left_trimmed = left_number.trim_start_matches('0');
|
||||
let right_trimmed = right_number.trim_start_matches('0');
|
||||
let left_significant = if left_trimmed.is_empty() {
|
||||
"0"
|
||||
} else {
|
||||
left_trimmed
|
||||
};
|
||||
let right_significant = if right_trimmed.is_empty() {
|
||||
"0"
|
||||
} else {
|
||||
right_trimmed
|
||||
};
|
||||
|
||||
let order = left_significant
|
||||
.len()
|
||||
.cmp(&right_significant.len())
|
||||
.then_with(|| left_significant.cmp(right_significant))
|
||||
.then_with(|| left_number.len().cmp(&right_number.len()));
|
||||
if order != Ordering::Equal {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
(Some(_), Some(_)) => {
|
||||
let left_char = left_chars.next().unwrap();
|
||||
let right_char = right_chars.next().unwrap();
|
||||
let order = left_char
|
||||
.to_lowercase()
|
||||
.cmp(right_char.to_lowercase())
|
||||
.then_with(|| left_char.cmp(&right_char));
|
||||
if order != Ordering::Equal {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take_digits<I>(characters: &mut std::iter::Peekable<I>) -> String
|
||||
where
|
||||
I: Iterator<Item = char>,
|
||||
{
|
||||
let mut digits = String::new();
|
||||
while characters.peek().is_some_and(char::is_ascii_digit) {
|
||||
digits.push(characters.next().unwrap());
|
||||
}
|
||||
digits
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn natural_order_places_two_before_ten() {
|
||||
let mut names = ["page10.jpg", "page2.jpg", "page1.jpg"];
|
||||
names.sort_by(|left, right| natural_cmp(left, right));
|
||||
assert_eq!(names, ["page1.jpg", "page2.jpg", "page10.jpg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn natural_order_handles_numbers_larger_than_integers() {
|
||||
assert_eq!(
|
||||
natural_cmp("9.zip", "123456789012345678901234567890.zip"),
|
||||
Ordering::Less
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_extension_is_case_insensitive() {
|
||||
assert!(is_zip_path(Path::new("comic.ZIP")));
|
||||
assert!(!is_zip_path(Path::new("comic.cbz")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_times_sort_after_known_times() {
|
||||
let now = SystemTime::now();
|
||||
assert_eq!(
|
||||
compare_optional_time(Some(now), None, SortDirection::Ascending),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
compare_optional_time(Some(now), None, SortDirection::Descending),
|
||||
Ordering::Less
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue