Skip to content

Commit 18d4b8c

Browse files
committed
feat(mangas): create image handler for future gallery resize
1 parent 2f5fa46 commit 18d4b8c

File tree

17 files changed

+681
-26
lines changed

17 files changed

+681
-26
lines changed

Cargo.lock

+416
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ midoku-macros = { path = "crates/midoku-macros" }
1616
midoku-path = { path = "crates/midoku-path" }
1717
midoku-store = { path = "crates/midoku-store" }
1818
midoku-theme = { path = "crates/midoku-theme" }
19+
const_format = "0.2.34"
1920
# dioxus = { git = "https://github.com/DioxusLabs/dioxus.git" }
2021
dioxus = "0.6.2"
2122
dioxus-free-icons = { version = "0.9", features = ["lucide"] }
2223
flate2 = "1.0.34"
24+
image = "0.25.5"
25+
rayon = "1.10.0"
2326
reqwest = { version = "0.12.12", default-features = false, features = [
2427
"rustls-tls",
2528
"charset",
@@ -31,6 +34,7 @@ serde_json = "1.0.134"
3134
tar = "0.4.43"
3235
thiserror = "2.0.9"
3336
tokio = "1.43.0"
37+
urlencoding = "2.1.3"
3438

3539
[package]
3640
name = "midoku"
@@ -46,15 +50,19 @@ midoku-config.workspace = true
4650
midoku-path.workspace = true
4751
midoku-store.workspace = true
4852
midoku-theme.workspace = true
53+
const_format.workspace = true
4954
dioxus = { workspace = true, features = ["router"] }
5055
dioxus-free-icons.workspace = true
5156
flate2.workspace = true
57+
image.workspace = true
58+
rayon.workspace = true
5259
reqwest = { workspace = true, features = ["json"] }
5360
serde.workspace = true
5461
serde_json.workspace = true
5562
tar.workspace = true
5663
thiserror.workspace = true
5764
tokio.workspace = true
65+
urlencoding.workspace = true
5866

5967
[features]
6068
default = ["desktop"]

crates/midoku-macros/src/lib.rs

+3-11
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,9 @@ impl ToTokens for Config {
4545
let identifier = self.dioxus.bundle.identifier.as_str();
4646

4747
tokens.extend(quote! {
48-
pub fn name() -> &'static str {
49-
#name
50-
}
51-
52-
pub fn version() -> &'static str {
53-
#version
54-
}
55-
56-
pub fn identifier() -> &'static str {
57-
#identifier
58-
}
48+
pub const NAME: &str = #name;
49+
pub const VERSION: &str = #version;
50+
pub const IDENTIFIER: &str = #identifier;
5951
});
6052
}
6153
}

crates/midoku-path/src/desktop.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,41 @@ use crate::error::{Error, Result};
66
pub fn app_config_dir() -> Result<PathBuf> {
77
dirs::config_dir()
88
.ok_or(Error::UnknownPath)
9-
.map(|dir| dir.join(midoku_config::identifier()))
9+
.map(|dir| dir.join(midoku_config::IDENTIFIER))
1010
}
1111

1212
/// Returns the path to the suggested directory for the app's data files.
1313
pub fn app_data_dir() -> Result<PathBuf> {
1414
dirs::data_dir()
1515
.ok_or(Error::UnknownPath)
16-
.map(|dir| dir.join(midoku_config::identifier()))
16+
.map(|dir| dir.join(midoku_config::IDENTIFIER))
1717
}
1818

1919
/// Returns the path to the suggested directory for the app's local data files.
2020
pub fn app_local_data_dir() -> Result<PathBuf> {
2121
dirs::data_local_dir()
2222
.ok_or(Error::UnknownPath)
23-
.map(|dir| dir.join(midoku_config::identifier()))
23+
.map(|dir| dir.join(midoku_config::IDENTIFIER))
2424
}
2525

2626
/// Returns the path to the suggested directory for the app's cache files.
2727
pub fn app_cache_dir() -> Result<PathBuf> {
2828
dirs::cache_dir()
2929
.ok_or(Error::UnknownPath)
30-
.map(|dir| dir.join(midoku_config::identifier()))
30+
.map(|dir| dir.join(midoku_config::IDENTIFIER))
3131
}
3232

3333
/// Returns the path to the suggested directory for the app's log files.
3434
pub fn app_log_dir() -> Result<PathBuf> {
3535
#[cfg(target_os = "macos")]
3636
let path = dirs::home_dir()
3737
.ok_or(Error::UnknownPath)
38-
.map(|dir| dir.join("Library/Logs").join(&midoku_config::identifier()));
38+
.map(|dir| dir.join("Library/Logs").join(&midoku_config::IDENTIFIER));
3939

4040
#[cfg(not(target_os = "macos"))]
4141
let path = dirs::data_local_dir()
4242
.ok_or(Error::UnknownPath)
43-
.map(|dir| dir.join(midoku_config::identifier()).join("logs"));
43+
.map(|dir| dir.join(midoku_config::IDENTIFIER).join("logs"));
4444

4545
path
4646
}

src/error.rs

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ pub enum Error {
2323
/// WebAssembly error.
2424
#[error("WASM error: {0}")]
2525
Wasm(String),
26+
27+
/// HTTP error.
28+
#[error("HTTP error: {0}")]
29+
Http(#[from] reqwest::Error),
30+
31+
/// Parse error.
32+
#[error("Parse error: {0}")]
33+
Parse(&'static str),
34+
35+
/// Image error.
36+
#[error("Image error: {0}")]
37+
Image(#[from] image::ImageError),
2638
}
2739

2840
pub type Result<T> = std::result::Result<T, Error>;

src/hook/gallery.rs

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use std::str::FromStr;
2+
3+
#[cfg(target_os = "android")]
4+
use dioxus::mobile::use_asset_handler;
5+
6+
#[cfg(not(target_os = "android"))]
7+
use dioxus::desktop::use_asset_handler;
8+
9+
#[doc(hidden)]
10+
macro_rules! response {
11+
($status:expr) => {{
12+
#[cfg(target_os = "android")]
13+
use dioxus::mobile::wry::http::Response;
14+
15+
#[cfg(not(target_os = "android"))]
16+
use dioxus::desktop::wry::http::Response;
17+
18+
Response::builder()
19+
.status($status)
20+
.body(Vec::new())
21+
.unwrap()
22+
}};
23+
($image_src:expr, $image_bytes:expr) => {{
24+
#[cfg(target_os = "android")]
25+
use dioxus::mobile::wry::http::Response;
26+
27+
#[cfg(not(target_os = "android"))]
28+
use dioxus::desktop::wry::http::Response;
29+
30+
Response::builder()
31+
.header("Content-Type", $image_src.format().to_mime_type())
32+
.body($image_bytes)
33+
.unwrap()
34+
}};
35+
}
36+
37+
fn get_value<'a>(query: &'a str, key: &str) -> Option<&'a str> {
38+
let start_pos = match query.find(&format!("{}=", key)) {
39+
Some(pos) => pos + key.len() + 1,
40+
None => return None,
41+
};
42+
let end_pos = match query[start_pos..].find("&") {
43+
Some(pos) => start_pos + pos,
44+
None => start_pos + query[start_pos..].len(),
45+
};
46+
Some(&query[start_pos..end_pos])
47+
}
48+
49+
fn get_decoded(query: &str, key: &str) -> Option<String> {
50+
get_value(query, key)
51+
.and_then(|value| urlencoding::decode(value).ok())
52+
.map(|value| value.to_string())
53+
}
54+
55+
fn get_int<T: FromStr>(query: &str, key: &str) -> Option<T> {
56+
get_value(query, key).and_then(|value| value.parse::<T>().ok())
57+
}
58+
59+
pub fn use_gallery_handler() {
60+
use_asset_handler("gallery", move |request, responder| {
61+
if request.method() != "GET" {
62+
return responder.respond(response!(404));
63+
}
64+
65+
let uri = request.uri();
66+
let query = uri.query().unwrap_or_default();
67+
68+
let image_url: String = match get_decoded(query, "url") {
69+
Some(url) => url.to_string(),
70+
None => return responder.respond(response!(400)),
71+
};
72+
73+
let width: u32 = match get_int(query, "width") {
74+
Some(value) => value,
75+
None => return responder.respond(response!(400)),
76+
};
77+
78+
let height: u32 = match get_int(query, "height") {
79+
Some(value) => value,
80+
None => return responder.respond(response!(400)),
81+
};
82+
83+
crate::util::thread::spawn!(async move {
84+
let image_bytes = crate::util::http::download_bytes(image_url).await;
85+
let Ok(image_bytes) = image_bytes else {
86+
return responder.respond(response!(404));
87+
};
88+
89+
let image_src = crate::util::image::Image::try_from(image_bytes);
90+
let image_src = image_src.and_then(|src| src.resize(width, height));
91+
let Ok(image_src) = image_src else {
92+
return responder.respond(response!(500));
93+
};
94+
95+
let Ok(image_bytes) = TryInto::<Vec<u8>>::try_into(&image_src) else {
96+
return responder.respond(response!(500));
97+
};
98+
99+
responder.respond(response!(image_src, image_bytes));
100+
});
101+
});
102+
}

src/hook/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod gallery;
12
mod state;
23

4+
pub use gallery::*;
35
pub use state::*;

src/main.rs

+18-2
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,33 @@ mod page;
66
mod state;
77
mod util;
88

9+
use std::sync::LazyLock;
10+
11+
use const_format::concatcp;
912
use dioxus::prelude::*;
13+
use rayon::ThreadPool;
1014

11-
use crate::hook::use_state_provider;
15+
use crate::hook::{use_gallery_handler, use_state_provider};
1216
use crate::layout::Navbar;
1317

1418
use crate::page::{
1519
extensions::ExtensionList,
1620
sources::{ChapterList, ChapterState, MangaList, MangaState, PageList, SourceList},
1721
};
1822

23+
const APP_USER_AGENT: &str = concatcp!(midoku_config::NAME, "/", midoku_config::VERSION);
1924
const CSS: Asset = asset!("/assets/tailwind.css");
2025

26+
const THREAD_POOL: LazyLock<ThreadPool> = LazyLock::new(|| {
27+
let num_threads = std::thread::available_parallelism()
28+
.map(|n| n.get())
29+
.unwrap_or(1);
30+
rayon::ThreadPoolBuilder::new()
31+
.num_threads(num_threads.min(4))
32+
.build()
33+
.expect("could not build thread pool.")
34+
});
35+
2136
#[derive(Debug, Clone, Routable, PartialEq)]
2237
#[rustfmt::skip]
2338
enum Route {
@@ -67,7 +82,7 @@ fn main() {
6782
use dioxus::desktop::{LogicalSize, WindowBuilder};
6883

6984
let window = WindowBuilder::default()
70-
.with_title(midoku_config::name())
85+
.with_title(midoku_config::NAME)
7186
.with_inner_size(LogicalSize::new(600, 1000));
7287

7388
let config = dioxus::desktop::Config::default()
@@ -80,6 +95,7 @@ fn main() {
8095

8196
#[component]
8297
fn App() -> Element {
98+
use_gallery_handler();
8399
use_state_provider();
84100

85101
#[cfg(not(target_os = "android"))]

src/model/extension.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct Extensions {
1818

1919
impl Extensions {
2020
pub async fn init() -> Self {
21-
let extensions_dir = crate::util::extensions_dir().unwrap();
21+
let extensions_dir = crate::util::path::extensions_dir().unwrap();
2222
std::fs::create_dir_all(extensions_dir.clone()).unwrap();
2323

2424
let mut extensions = BTreeMap::new();

src/page/sources/mangas.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ pub fn MangaList(extension_id: String) -> Element {
5757
}
5858
});
5959

60+
const WIDTH: u32 = 300;
61+
const HEIGHT: u32 = 450;
62+
6063
rsx! {
6164
Header { title: name.clone() }
6265
Grid {
@@ -65,7 +68,7 @@ pub fn MangaList(extension_id: String) -> Element {
6568
extension_id: extension_id.clone(),
6669
manga_id: manga.id.clone(),
6770
ItemImage {
68-
src: manga.cover_url.clone(),
71+
src: format!("/gallery/?url={}&width={WIDTH}&height={HEIGHT}", manga.cover_url.clone()),
6972
alt: manga.title.clone(),
7073
}
7174
ItemTitle { title: manga.title.clone() }

src/page/sources/pages.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,11 @@ pub fn PageList(extension_id: String, manga_id: String, chapter_id: String) -> E
6666
});
6767

6868
rsx! {
69-
div { GoBackButton {
70-
Icon { style: "color: inherit", icon: LdArrowLeft }
71-
} }
69+
div {
70+
GoBackButton {
71+
Icon { style: "color: inherit", icon: LdArrowLeft }
72+
}
73+
}
7274
ul { id: "page-view",
7375
for page in page_list.read().iter() {
7476
Page {

src/state/extensions.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ impl StateExtensions for State {
1515
async fn install_extension(&mut self, manifest: &Manifest) -> Result<()> {
1616
let repository_url = self.repository_url();
1717

18-
let extensions_dir = crate::util::extensions_dir().unwrap();
18+
let extensions_dir = crate::util::path::extensions_dir().unwrap();
1919
let extension_path = extensions_dir.join(&manifest.id);
2020

2121
// If the path exists, then the extensions have already been installed.
@@ -50,7 +50,7 @@ impl StateExtensions for State {
5050
}
5151

5252
async fn uninstall_extension(&mut self, extension_id: &str) -> Result<()> {
53-
let extensions_dir = crate::util::extensions_dir().unwrap();
53+
let extensions_dir = crate::util::path::extensions_dir().unwrap();
5454
let extension_path = extensions_dir.join(extension_id);
5555

5656
// Remove the extension directory

src/util/http.rs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use std::sync::LazyLock;
2+
3+
use reqwest::Client;
4+
5+
use crate::error::Result;
6+
use crate::APP_USER_AGENT;
7+
8+
const CLIENT: LazyLock<Client> = LazyLock::new(|| {
9+
Client::builder()
10+
.user_agent(APP_USER_AGENT)
11+
.build()
12+
.expect("could not build http client.")
13+
});
14+
15+
pub async fn download_bytes<S: AsRef<str>>(url: S) -> Result<Vec<u8>> {
16+
let url = url.as_ref();
17+
let response = CLIENT.get(url).send().await?;
18+
19+
let raw = response.bytes().await?;
20+
let bytes = raw.to_vec();
21+
22+
Ok(bytes)
23+
}

0 commit comments

Comments
 (0)