mod jnovel; 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"); let jnc = self.jnc.clone(); self.jnc.token = None; self.jnc.login_state = jnovel::LoginState::LoggedOut; self.jnc.library_state = jnovel::LibraryState::Unloaded; Command::perform(jnovel::State::logout(jnc), |()| 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; Command::none() } else { self.jnc.login_state = status; Command::none() } } jnovel::Message::LibraryState(status) => { match status { jnovel::LibraryState::Unloaded => { Command::none() } jnovel::LibraryState::Loading(progress) => { println!("Loading: {}", progress); Command::none() } jnovel::LibraryState::Loaded(data) => { self.jnc.library = data; println!("Loaded"); Command::none() } jnovel::LibraryState::Idle => { Command::none() } jnovel::LibraryState::Error(err) => { println!("{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(_) => { let mut col = Column::new(); col = col.push(button("Logout").on_press(Message::JncAction(jnovel::Message::Logout))); for (id, entry) in &self.jnc.library { col = col.push(button(entry.info.title.as_str()).on_press(Message::JncAction(jnovel::Message::LoginState(jnovel::LoginState::LoggedOut)))); } Element::from(col)}, 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(), }; let library_loading = match self.jnc.login_state { jnovel::LoginState::Success(token) => { match self.jnc.library_state { jnovel::LibraryState::Unloaded => { jnovel::State::load_library(token.clone()) .map(|status| Message::JncAction(jnovel::Message::LibraryState(status))) }, jnovel::LibraryState::Loading(progress) => { jnovel::State::load_library(token.clone()) .map(|status| Message::JncAction(jnovel::Message::LibraryState(status))) }, _ => Subscription::none() } }, _ => Subscription::none() }; Subscription::batch([ system_theme, jnc_otp_check, library_loading ]) } } 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()) }