Initial GUI with J-Novel CLub Login (#9)
All checks were successful
Run Tests on Code / run-tests (push) Successful in 18s
All checks were successful
Run Tests on Code / run-tests (push) Successful in 18s
This commit is contained in:
commit
2652933bbb
5 changed files with 5057 additions and 4 deletions
4469
Cargo.lock
generated
4469
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -6,3 +6,12 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
iced = { version = "^0.10", features = ["tokio", "async-std", "smol"]}
|
||||||
|
dark-light = "^1.0.0"
|
||||||
|
tokio = { version = "1.35.1", features = ["rt", "rt-multi-thread", "macros", "time"] }
|
||||||
|
reqwest = { version = "0.11.23", features = ["blocking", "json"] }
|
||||||
|
serde_json = "1.0.111"
|
||||||
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
arrayvec = { version = "^0.7.4", features = ["serde"] }
|
||||||
|
chrono = "^0.4.31"
|
||||||
|
open = "^5.0.1"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Handy Tool for quickly importing bought J-Novel Club Light Novels into Calibre Web.
|
Handy Tool for quickly importing bought J-Novel Club Light Novels into Calibre Web.
|
||||||
|
|
||||||
Built using Rust and CXX-QT
|
Built using Rust and iced
|
||||||
|
|
||||||
Planned Features:
|
Planned Features:
|
||||||
- [ ] Import bought novels directly into your Calibre Web Server, metadata included
|
- [ ] Import bought novels directly into your Calibre Web Server, metadata included
|
||||||
|
|
305
src/jnovel.rs
Normal file
305
src/jnovel.rs
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use arrayvec::ArrayString;
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
macro_rules! api {
|
||||||
|
() => {String::from("https://labs.j-novel.club/")};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) enum Message {
|
||||||
|
Login,
|
||||||
|
Logout,
|
||||||
|
LoginState(LoginState),
|
||||||
|
LibraryState(LibraryState),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||||
|
pub(crate) struct State {
|
||||||
|
pub(crate) token: Option<ArrayString<32>>,
|
||||||
|
pub(crate) login_state: LoginState,
|
||||||
|
pub(crate) library_state: LibraryState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub(crate) async fn login() -> LoginState {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let res = match client.get(api!() + "app/v1/auth/otp4app/generate?format=json")
|
||||||
|
.send()
|
||||||
|
.await {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => return LoginState::Error(e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match res.text().await {
|
||||||
|
Ok(otp_data) => {
|
||||||
|
let otp = match serde_json::from_str(&otp_data) {
|
||||||
|
Ok(otp) => otp,
|
||||||
|
Err(e) => return LoginState::Error(e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = Local::now();
|
||||||
|
|
||||||
|
LoginState::AwaitingConfirmation(otp, start)
|
||||||
|
},
|
||||||
|
Err(e) => LoginState::Error(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn logout(state: State) {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let url = api!() + format!("app/v1/auth/logout?{:?}", state.token).as_str();
|
||||||
|
|
||||||
|
let _ = client.post(url).send().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load_library(state: State) -> LibraryState {
|
||||||
|
/*
|
||||||
|
1: lib = https://labs.j-novel.club/app/v1/me/library
|
||||||
|
2: every vol in lib.books serie = https://labs.j-novel.club/app/v1/volumes/{vol.volume.legacyId}/serie?format=json
|
||||||
|
3: ser[serie.slug].info = serie
|
||||||
|
4: ser[serie.slug].volumes.insert(vol)
|
||||||
|
*/
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let url = api!() + format!("app/v1/me/library?format=json&{:?}", state.token).as_str();
|
||||||
|
|
||||||
|
println!("{url}");
|
||||||
|
|
||||||
|
let res = match client.get(url)
|
||||||
|
.send()
|
||||||
|
.await {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => return LibraryState::Error(e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match res.text().await {
|
||||||
|
Ok(text) => {
|
||||||
|
let library = match serde_json::from_str(&text) {
|
||||||
|
Ok(library) => library,
|
||||||
|
Err(e) => return LibraryState::Error(e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
LibraryState::Loaded
|
||||||
|
},
|
||||||
|
Err(e) => LibraryState::Error(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize)]
|
||||||
|
pub(crate) enum LibraryState {
|
||||||
|
#[default]
|
||||||
|
Unloaded,
|
||||||
|
Loading,
|
||||||
|
Loaded,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize)]
|
||||||
|
pub(crate) struct Otp {
|
||||||
|
pub(crate) otp: ArrayString<6>,
|
||||||
|
pub(crate) proof: ArrayString<6>,
|
||||||
|
pub(crate) ttl: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Otp {
|
||||||
|
pub(crate) async fn check(state: OtpState) -> (LoginState, OtpState) {
|
||||||
|
match state {
|
||||||
|
OtpState::Starting(otp, start) => {
|
||||||
|
(LoginState::AwaitingConfirmation(otp, start), OtpState::Waiting(otp, start))
|
||||||
|
}
|
||||||
|
OtpState::Waiting(otp, start) => {
|
||||||
|
let url = api!() + format!("app/v1/auth/otp4app/check/{}/{}?format=json", otp.otp, otp.proof).as_str();
|
||||||
|
|
||||||
|
let res = match reqwest::get(url).await {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => return (LoginState::Error(e.to_string()), OtpState::Error(e.to_string()))
|
||||||
|
};
|
||||||
|
|
||||||
|
match res.status() {
|
||||||
|
StatusCode::NO_CONTENT => {
|
||||||
|
(LoginState::AwaitingConfirmation(otp, start), OtpState::Waiting(otp, start))
|
||||||
|
}
|
||||||
|
StatusCode::OK => {
|
||||||
|
let text = match res.text().await {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => return (LoginState::Error(e.to_string()), OtpState::Error(e.to_string()))
|
||||||
|
};
|
||||||
|
|
||||||
|
match serde_json::from_str::<OtpToken>(text.as_str()) {
|
||||||
|
Ok(data) => {
|
||||||
|
let token = data.id.get(3..35).expect("Token should fill the ArrayString<36>").parse().expect("Should fit ArrayString<32>");
|
||||||
|
(LoginState::Success(token), OtpState::Valid(token))
|
||||||
|
},
|
||||||
|
Err(e) => (LoginState::Error(e.to_string()), OtpState::Error(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
(LoginState::Error("Timed Out".to_owned()), OtpState::Error("Timed Out".to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OtpState::Error(error) => {
|
||||||
|
(LoginState::Error(error.clone()), OtpState::Error(error))
|
||||||
|
}
|
||||||
|
OtpState::Valid(token) => {
|
||||||
|
(LoginState::Success(token), OtpState::Valid(token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||||
|
pub(crate) struct OtpToken {
|
||||||
|
pub(crate) id: ArrayString<36>,
|
||||||
|
pub(crate) ttl: String,
|
||||||
|
pub(crate) created: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub(crate) enum OtpState {
|
||||||
|
Starting(Otp, DateTime<Local>),
|
||||||
|
Waiting(Otp, DateTime<Local>),
|
||||||
|
Error(String),
|
||||||
|
Valid(ArrayString<32>)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||||
|
pub(crate) enum LoginState {
|
||||||
|
#[default]
|
||||||
|
LoggedOut,
|
||||||
|
LoggingIn,
|
||||||
|
Success(ArrayString<32>),
|
||||||
|
AwaitingConfirmation(Otp, DateTime<Local>),
|
||||||
|
Error(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for LoginState {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let text = match self {
|
||||||
|
Self::LoggedOut => "Logged Out".to_owned(),
|
||||||
|
Self::Success(_) => "Logged In".to_owned(),
|
||||||
|
Self::LoggingIn => "Logging In".to_owned(),
|
||||||
|
Self::AwaitingConfirmation(_, _) => "Awaiting Code Confirmation".to_owned(),
|
||||||
|
Self::Error(message) => format!("Error during Login: {message}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{text}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ApiLibrary {
|
||||||
|
books: Vec<LibraryVolume>,
|
||||||
|
pagination: Pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Pagination {
|
||||||
|
limit: usize,
|
||||||
|
skip: usize,
|
||||||
|
#[serde(alias="lastPage")]
|
||||||
|
last_page: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Series {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LibraryVolume {
|
||||||
|
id: String,
|
||||||
|
#[serde(alias="legacyId")]
|
||||||
|
legacy_id: String,
|
||||||
|
volume: Volume,
|
||||||
|
series: Option<String>,
|
||||||
|
purchased: String,
|
||||||
|
#[serde(alias="lastDownloaded")]
|
||||||
|
last_downloaded: Option<String>,
|
||||||
|
#[serde(alias="lastUpdated")]
|
||||||
|
last_updated: Option<String>,
|
||||||
|
downloads: Vec<Download>,
|
||||||
|
status: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Volume {
|
||||||
|
id: String,
|
||||||
|
#[serde(alias="legacyId")]
|
||||||
|
legacy_id: String,
|
||||||
|
title: String,
|
||||||
|
#[serde(alias="originalTitle")]
|
||||||
|
original_title: String,
|
||||||
|
slug: String,
|
||||||
|
number: usize,
|
||||||
|
#[serde(alias="OriginalPublisher")]
|
||||||
|
original_publisher: String,
|
||||||
|
label: Label,
|
||||||
|
creators: Vec<Creator>,
|
||||||
|
hidden: bool,
|
||||||
|
#[serde(alias="forumTopicId")]
|
||||||
|
forum_topic_id: usize,
|
||||||
|
created: String,
|
||||||
|
publising: String,
|
||||||
|
description: String,
|
||||||
|
#[serde(alias="shortDescription")]
|
||||||
|
short_description: String,
|
||||||
|
cover: Cover,
|
||||||
|
owned: bool,
|
||||||
|
#[serde(alias="premiumExtras")]
|
||||||
|
premium_extras: String,
|
||||||
|
#[serde(alias="noEbook")]
|
||||||
|
no_ebook: bool,
|
||||||
|
#[serde(alias="totalParts")]
|
||||||
|
total_parts: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Cover {
|
||||||
|
#[serde(alias="originalUrl")]
|
||||||
|
original_url: String,
|
||||||
|
#[serde(alias="coverUrl")]
|
||||||
|
cover_url: String,
|
||||||
|
#[serde(alias="thumbnailUrl")]
|
||||||
|
thumbnail_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Creator {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
role: Role,
|
||||||
|
#[serde(alias="originalName")]
|
||||||
|
original_name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Download {
|
||||||
|
link: String,
|
||||||
|
r#type: FileType,
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
enum Label {
|
||||||
|
Heart,
|
||||||
|
Club,
|
||||||
|
Pulp
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
enum FileType {
|
||||||
|
EPUB
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
enum Role {
|
||||||
|
AUTHOR,
|
||||||
|
ILLUSTRATOR,
|
||||||
|
TRANSLATOR,
|
||||||
|
EDITOR
|
||||||
|
}
|
276
src/main.rs
276
src/main.rs
|
@ -1,3 +1,275 @@
|
||||||
fn main() {
|
mod jnovel;
|
||||||
println!("Hello, world!");
|
|
||||||
|
use iced::{Application, Command, Element, executor, Length, Padding, Pixels, Settings, Subscription, subscription, Theme, time};
|
||||||
|
use iced::widget::{button, column, text, Column, container, toggler::{Toggler}, pick_list, row, Row, text_input};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use chrono::{Local};
|
||||||
|
use serde::{Deserialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct CalibreWebImporter {
|
||||||
|
jnc: jnovel::State,
|
||||||
|
settings: AppSettings,
|
||||||
|
current_menu: Menu,
|
||||||
|
previous_menu: Menu,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CalibreWebImporter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
jnc: jnovel::State::default(),
|
||||||
|
settings: AppSettings::default(),
|
||||||
|
current_menu: Menu::JNovel,
|
||||||
|
previous_menu: Menu::JNovel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AppSettings {
|
||||||
|
theme: AppTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
theme: AppTheme::System
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Menu {
|
||||||
|
Settings,
|
||||||
|
JNovel,
|
||||||
|
Calibre
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
enum AppTheme {
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
System
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AppTheme {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let text = match self {
|
||||||
|
AppTheme::Light => "Light",
|
||||||
|
AppTheme::Dark => "Dark",
|
||||||
|
AppTheme::System => "System",
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{text}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppTheme {
|
||||||
|
fn get_iced_theme(&self) -> Theme {
|
||||||
|
match self {
|
||||||
|
AppTheme::Light => Theme::Light,
|
||||||
|
AppTheme::Dark => Theme::Dark,
|
||||||
|
AppTheme::System => system_theme_mode().get_iced_theme(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
JncAction(jnovel::Message),
|
||||||
|
SetTheme(AppTheme),
|
||||||
|
Navigate(Menu),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application for CalibreWebImporter {
|
||||||
|
type Executor = executor::Default;
|
||||||
|
type Message = Message;
|
||||||
|
type Theme = Theme;
|
||||||
|
type Flags = ();
|
||||||
|
|
||||||
|
fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
settings: AppSettings {
|
||||||
|
theme: AppTheme::System,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Command::none()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn title(&self) -> String {
|
||||||
|
String::from("Calibre Web Importer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Command<Message> {
|
||||||
|
match message {
|
||||||
|
Message::JncAction(jnc_message) => {
|
||||||
|
match jnc_message {
|
||||||
|
jnovel::Message::Login => {
|
||||||
|
self.jnc.login_state = jnovel::LoginState::LoggingIn;
|
||||||
|
let _ = open::that("https://j-novel.club/user/otp");
|
||||||
|
Command::perform(jnovel::State::login(), |status| Message::JncAction(jnovel::Message::LoginState(status)))
|
||||||
|
}
|
||||||
|
jnovel::Message::Logout => {
|
||||||
|
println!("Logging out from J-Novel Club");
|
||||||
|
self.jnc.token = None;
|
||||||
|
self.jnc.login_state = jnovel::LoginState::LoggedOut;
|
||||||
|
Command::perform(jnovel::State::logout(self.jnc.clone()), |()| Message::JncAction(jnovel::Message::LoginState(jnovel::LoginState::LoggedOut)))
|
||||||
|
}
|
||||||
|
jnovel::Message::LoginState(status) => {
|
||||||
|
if let jnovel::LoginState::Success(token) = status {
|
||||||
|
self.jnc.token = Some(token);
|
||||||
|
self.jnc.login_state = status;
|
||||||
|
|
||||||
|
if self.jnc.library_state == jnovel::LibraryState::Unloaded {
|
||||||
|
Command::perform(jnovel::State::load_library(self.jnc.clone()), |status| Message::JncAction(jnovel::Message::LibraryState(status)))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.jnc.login_state = status;
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jnovel::Message::LibraryState(status) => {
|
||||||
|
match status {
|
||||||
|
jnovel::LibraryState::Unloaded => {
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
jnovel::LibraryState::Loading => {
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
jnovel::LibraryState::Loaded => {
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
jnovel::LibraryState::Error(err) => {
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::Navigate(menu) => {
|
||||||
|
self.previous_menu = self.current_menu;
|
||||||
|
self.current_menu = menu;
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
Message::SetTheme(theme) => {
|
||||||
|
self.settings.theme = theme;
|
||||||
|
Command::none()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn view(&self) -> Element<Message> {
|
||||||
|
// TODO: add collapsible sidebar for navigation
|
||||||
|
|
||||||
|
let menu_content = match self.current_menu {
|
||||||
|
Menu::Settings => {
|
||||||
|
let theme_options = vec![AppTheme::Light, AppTheme::Dark, AppTheme::System];
|
||||||
|
let theme_selector = pick_list(theme_options, Some(self.settings.theme.clone()), Message::SetTheme);
|
||||||
|
|
||||||
|
column(vec![
|
||||||
|
theme_selector.into(),
|
||||||
|
button("Close").on_press(Message::Navigate(self.previous_menu)).into()
|
||||||
|
])
|
||||||
|
},
|
||||||
|
Menu::JNovel => {
|
||||||
|
let jnc_login_button: Element<_, _> = match &self.jnc.login_state {
|
||||||
|
jnovel::LoginState::Success(_) => Element::from(button("Logout").on_press(Message::JncAction(jnovel::Message::Logout))),
|
||||||
|
jnovel::LoginState::LoggedOut => Element::from(button("Login").on_press(Message::JncAction(jnovel::Message::Login))),
|
||||||
|
jnovel::LoginState::LoggingIn => Element::from(text("Logging in")),
|
||||||
|
jnovel::LoginState::AwaitingConfirmation(code, start) => Element::from(text(format!("Login Code: {}. Expiring: {}s", code.otp, 600 - (Local::now() - start).num_seconds()))),
|
||||||
|
jnovel::LoginState::Error(err) => Element::from(row![
|
||||||
|
button("Error").on_press(Message::JncAction(jnovel::Message::LoginState(jnovel::LoginState::LoggedOut))),
|
||||||
|
text(err),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
column(vec![Element::from(
|
||||||
|
row(vec![
|
||||||
|
jnc_login_button.into()
|
||||||
|
])
|
||||||
|
)])
|
||||||
|
},
|
||||||
|
Menu::Calibre => {
|
||||||
|
column(vec![
|
||||||
|
text("Not Implemented").into()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(Padding::new(10.0));
|
||||||
|
|
||||||
|
let mut menu = Column::new();
|
||||||
|
|
||||||
|
menu = menu
|
||||||
|
.push(button("Settings").on_press(Message::Navigate(Menu::Settings)))
|
||||||
|
.push(button("JNC").on_press(Message::Navigate(Menu::JNovel)))
|
||||||
|
.push(button("Calibre").on_press(Message::Navigate(Menu::Calibre)))
|
||||||
|
.width(Length::Shrink)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.padding(Padding::new(10.0))
|
||||||
|
.spacing(Pixels(10.0));
|
||||||
|
|
||||||
|
let app_content: Row<Message> = row(vec![menu.into(), menu_content.into()])
|
||||||
|
.width(Length::Fill)
|
||||||
|
.spacing(Pixels(10.0));
|
||||||
|
|
||||||
|
container(app_content)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.center_x()
|
||||||
|
.center_y()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> Self::Theme {
|
||||||
|
self.settings.theme.get_iced_theme()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
|
let system_theme = match self.settings.theme == AppTheme::System {
|
||||||
|
true => time::every(time::Duration::from_secs(1))
|
||||||
|
.map(|_| Message::SetTheme(AppTheme::System)),
|
||||||
|
false => Subscription::none()
|
||||||
|
};
|
||||||
|
|
||||||
|
let jnc_otp_check = match self.jnc.login_state.clone() {
|
||||||
|
jnovel::LoginState::AwaitingConfirmation(otp, start) => {
|
||||||
|
subscription::unfold(
|
||||||
|
"otp_login",
|
||||||
|
jnovel::OtpState::Starting(otp, start),
|
||||||
|
|state| async move {
|
||||||
|
jnovel::Otp::check(state).await
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.map(|state| Message::JncAction(jnovel::Message::LoginState(state)))
|
||||||
|
},
|
||||||
|
_ => Subscription::none(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Subscription::batch([
|
||||||
|
system_theme,
|
||||||
|
jnc_otp_check
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_theme_mode() -> AppTheme {
|
||||||
|
match dark_light::detect() {
|
||||||
|
dark_light::Mode::Light | dark_light::Mode::Default => AppTheme::Light,
|
||||||
|
dark_light::Mode::Dark => AppTheme::Dark,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> iced::Result {
|
||||||
|
CalibreWebImporter::run(Settings::default())
|
||||||
}
|
}
|
Reference in a new issue