305 lines
No EOL
10 KiB
Rust
305 lines
No EOL
10 KiB
Rust
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())
|
|
} |