This commit is contained in:
Domipoke
2026-04-01 02:16:25 +02:00
parent 2f90d71911
commit 758dc3fad2
7 changed files with 141 additions and 36 deletions
View File
+4
View File
@@ -1,3 +1,7 @@
[[Animeworld]] [[Animeworld]]
name="Frieren: Beyond Journey's End 2" name="Frieren: Beyond Journey's End 2"
episodes="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx" episodes="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx"
[[Animeworld]]
name="One Piece"
episodes="https://www.animeworld.ac/play/one-piece-subita.qzG-LE/HPKmX1"
+18 -7
View File
@@ -200,7 +200,8 @@ pub mod list {
} }
pub fn handle_input(&mut self, kc: ratatui::crossterm::event::KeyCode) -> bool { pub fn handle_input(&mut self, kc: ratatui::crossterm::event::KeyCode) -> bool {
if let Some(gcw) = self.get_current_widget() { if let Some(gcw) = self.get_current_widget() {
return (gcw.handle_input)(kc); let f = (gcw.handle_input)(kc);
return f;
} }
false false
} }
@@ -328,6 +329,16 @@ pub mod Library {
trasmitter_to_app, trasmitter_to_app,
} }
} }
pub fn sorted_keys_from_hash(hs: &HashMap<String, EpisodeListItem>) -> Vec<&String> {
let mut items: Vec<&String> = hs.keys().collect::<Vec<&String>>();
items.sort_by(|a: &&String,b: &&String| {
if let Ok(ai) = a.parse::<isize>() && let Ok(bi) = b.parse::<isize>() {
return bi.cmp(&ai);
}
return b.cmp(a);
}); // ? REVERSE SORT - MAX TO MIN -> NEWEST FIRST
items
}
} }
pub enum LibraryStateStatus { pub enum LibraryStateStatus {
Empty, Empty,
@@ -386,7 +397,7 @@ pub mod Library {
state.lib = lib; state.lib = lib;
state.status = LibraryStateStatus::Lib(0); state.status = LibraryStateStatus::Lib(0);
state.entry_selected = false; state.entry_selected = false;
return Paragraph::new("Lib loaded").block(block).render(area, buf); return Paragraph::new("Lib loading").block(block).render(area, buf);
} else { } else {
return Paragraph::new("No lib found. Edit ./library/animelibrary.toml or ./library/mangalibrary.toml. Format:\n[[Animeworld]]\nname=\"Frieren\"\nepisodes=\"https://url_to_a_generic_episode_of_animeworld_frieren_or_chapter_list_manga_page/\"").block(block).render(area, buf); return Paragraph::new("No lib found. Edit ./library/animelibrary.toml or ./library/mangalibrary.toml. Format:\n[[Animeworld]]\nname=\"Frieren\"\nepisodes=\"https://url_to_a_generic_episode_of_animeworld_frieren_or_chapter_list_manga_page/\"").block(block).render(area, buf);
} }
@@ -408,7 +419,7 @@ pub mod Library {
let key = *k; let key = *k;
if let Some(a) = state.lib.get(key) { if let Some(a) = state.lib.get(key) {
state.current_source = Some(key.clone()); state.current_source = Some(key.clone());
let list = List::new(a.iter().map(|li| ListItem::new(li.name.clone())).collect::<Vec<ListItem>>()).block(block_sc); let list = List::new(a.iter().map(|li| ListItem::new(li.name.clone())).collect::<Vec<ListItem>>()).block(block_sc).highlight_symbol(">> ");
StatefulWidget::render(list, lay[0], buf, &mut state.list_state_source); StatefulWidget::render(list, lay[0], buf, &mut state.list_state_source);
} else { } else {
Widget::render(block_sc, lay[0], buf); Widget::render(block_sc, lay[0], buf);
@@ -417,13 +428,13 @@ pub mod Library {
Widget::render(block_sc, lay[0], buf); Widget::render(block_sc, lay[0], buf);
} }
if state.entry_selected && let Some(eps_hash) = &state.episodes_loaded { if state.entry_selected && let Some(eps_hash) = &state.episodes_loaded {
let mut items = eps_hash.keys().collect::<Vec<&String>>(); let items = Self::sorted_keys_from_hash(eps_hash);
items.sort_by(|a,b| b.cmp(a)); // ? REVERSE SORT - MAX TO MIN -> NEWEST FIRST
let mut eps = Vec::with_capacity(items.len()); let mut eps = Vec::with_capacity(items.len());
for it in items { for it in items {
eps.push(ListItem::new(it.clone())); let s = eps_hash.get(it).and_then(|f| Some(f.title.clone())).unwrap_or(it.clone());
eps.push(ListItem::new(s));
} }
let list_ep = List::new(eps).block(block_eps); let list_ep = List::new(eps).block(block_eps).highlight_symbol(">> ");;
StatefulWidget::render(list_ep, lay[1], buf, &mut state.list_state_eps); StatefulWidget::render(list_ep, lay[1], buf, &mut state.list_state_eps);
} else { } else {
Widget::render(block_eps, lay[1], buf); Widget::render(block_eps, lay[1], buf);
+87 -14
View File
@@ -6,7 +6,7 @@ mod components;
mod source; mod source;
mod utils; mod utils;
mod folders; mod folders;
use std::{any::Any, cell::RefCell, collections::HashMap, process::{Command, Stdio}, rc::Rc, sync::Arc, time::Duration, vec}; use std::{any::Any, cell::RefCell, cmp, collections::HashMap, process::{Command, Stdio}, rc::Rc, sync::Arc, time::Duration, vec};
use color_eyre::{Result}; use color_eyre::{Result};
use ratatui::{ use ratatui::{
DefaultTerminal, Frame, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Direction, Layout, Rect}, style::Style, text::{Line, Span}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph} DefaultTerminal, Frame, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Direction, Layout, Rect}, style::Style, text::{Line, Span}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}
@@ -183,6 +183,7 @@ impl App {
match self.library.rx.try_recv() { match self.library.rx.try_recv() {
Ok(h)=>{ Ok(h)=>{
self.library.library_list_state.borrow_mut().episodes_loaded = Some(h); self.library.library_list_state.borrow_mut().episodes_loaded = Some(h);
self.library.library_list_state.borrow_mut().entry_selected = true;
} }
Err(_) => {} Err(_) => {}
} }
@@ -224,7 +225,7 @@ impl App {
} else { } else {
self.navigation_menu.handle_input(k); self.navigation_menu.handle_input(k);
} }
} },
KeyCode::Enter => { KeyCode::Enter => {
if self.navigation_menu.lastpath.is_empty() { if self.navigation_menu.lastpath.is_empty() {
self.navigation_menu.enter(); self.navigation_menu.enter();
@@ -458,11 +459,22 @@ impl App {
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(Style::new().fg(ratatui::style::Color::Cyan)); .border_style(Style::new().fg(ratatui::style::Color::Cyan));
let lay = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Percentage(100),
Constraint::Min(3)
]).split(area);
let mut state = render_ls.borrow_mut(); let mut state = render_ls.borrow_mut();
let l = Library::Library::new(lib_type,tx_clone_render.clone()); //TODO let l = Library::Library::new(lib_type,tx_clone_render.clone()); //TODO
let cmd_list = Self::generate_cmd_list(vec![
(CmdListType::Multi, "W,A,S,D or Arrows","to move"),
(CmdListType::Single, "Enter","select"),
(CmdListType::Single, "Esc","Go Back"),
]);
frame.render_widget( cmd_list, lay[1]);
//frame.render_widget(b, area); //frame.render_widget(b, area);
frame.render_stateful_widget(l, area, &mut *state); frame.render_stateful_widget(l, lay[0], &mut *state);
}), handle_input: Box::new(move |k: KeyCode| { }), handle_input: Box::new(move |k: KeyCode| {
let mut state = input_ls.borrow_mut(); let mut state = input_ls.borrow_mut();
let tx_clone_input_clone = tx_clone_input.clone(); let tx_clone_input_clone = tx_clone_input.clone();
@@ -470,36 +482,90 @@ impl App {
LibraryStateStatus::Lib(p) => { LibraryStateStatus::Lib(p) => {
match k { match k {
KeyCode::Left | KeyCode::Char('a') => { KeyCode::Left | KeyCode::Char('a') => {
if state.entry_selected {
} else {
state.list_state_source.select(None); state.list_state_source.select(None);
if p>0 {
state.status = LibraryStateStatus::Lib(p-1); state.status = LibraryStateStatus::Lib(p-1);
true } else {
state.status = LibraryStateStatus::Lib(state.lib.keys().len()-1);
} }
}
true
},
KeyCode::Right | KeyCode::Char('d') => { KeyCode::Right | KeyCode::Char('d') => {
if state.entry_selected {
} else {
state.list_state_source.select(None); state.list_state_source.select(None);
state.status = LibraryStateStatus::Lib(p+1); state.status = LibraryStateStatus::Lib(p+1);
true
} }
true
},
KeyCode::Up | KeyCode::Char('w') => { KeyCode::Up | KeyCode::Char('w') => {
if state.entry_selected { if state.entry_selected {
if state.list_state_eps.selected().is_none() {
state.list_state_eps.select_last();
}else {
state.list_state_eps.select_previous(); state.list_state_eps.select_previous();
}
} else {
if state.list_state_source.selected().is_none() {
state.list_state_source.select_last();
}else { }else {
state.list_state_source.select_previous(); state.list_state_source.select_previous();
} }
}
true true
}, },
KeyCode::Down | KeyCode::Char('s')=> { KeyCode::Down | KeyCode::Char('s')=> {
if state.entry_selected { if state.entry_selected {
if state.list_state_eps.selected().is_none() {
state.list_state_eps.select_first();
}else {
state.list_state_eps.select_next(); state.list_state_eps.select_next();
}
} else {
if state.list_state_source.selected().is_none() {
state.list_state_source.select_first();
}else { }else {
state.list_state_source.select_next(); state.list_state_source.select_next();
} }
}
true true
}, },
KeyCode::Enter => { KeyCode::Enter => {
if state.entry_selected {
} else {
let state_bo = &state; let state_bo = &state;
if state_bo.entry_selected {
let kind = match lib_type {
Library::LibraryType::Anime => SourceType::Anime,
Library::LibraryType::Manga => SourceType::Manga
};
if let Some(u) = state_bo.list_state_eps.selected() && let Some(current) = state_bo.current_source.clone() && let Some(episode_loaded) = &state_bo.episodes_loaded {
let items =Library::Library::sorted_keys_from_hash(&episode_loaded);
if let Some(item) = items.get(u) {
//source.episode_play_url(url);
if let Some(eli) = episode_loaded.get(*item).clone() {
let eli_c = (*eli).clone();
tokio::spawn(async move {
if let Ok(ex) = Extension::buildfromName(kind, &current){
match kind {
SourceType::Anime => {
let s_a = ex.toAnimeSource();
let _ = s_a.lib_ep_select(eli_c).await;
}
SourceType::Manga => {
let s_m = ex.toMangaSource();
let _ = s_m.lib_ep_select(eli_c).await;
}
};
}
});
}
}
}
} else {
if let Some(sel) = state_bo.list_state_source.selected() && let Some(current) = state_bo.current_source.clone() { if let Some(sel) = state_bo.list_state_source.selected() && let Some(current) = state_bo.current_source.clone() {
let kind = match lib_type { let kind = match lib_type {
Library::LibraryType::Anime => SourceType::Anime, Library::LibraryType::Anime => SourceType::Anime,
@@ -508,21 +574,21 @@ impl App {
if let Some(u_list) = state_bo.lib.get(&current) { if let Some(u_list) = state_bo.lib.get(&current) {
if let Some(u) = u_list.get(sel) { if let Some(u) = u_list.get(sel) {
let u_clone = u.clone(); let u_clone = u.clone();
let lib_type_clone = lib_type.clone(); // let lib_type_clone = lib_type.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(ex) = Extension::buildfromName(kind, &current) { if let Ok(ex) = Extension::buildfromName(kind, &current) {
let hs = match lib_type_clone { let hs = match kind {
Library::LibraryType::Anime => { SourceType::Anime => {
let s: Box<dyn AnimeSource + Send + Sync> = ex.toAnimeSource(); let s: Box<dyn AnimeSource + Send + Sync> = ex.toAnimeSource();
s.lib_url_parse(u_clone.episodes).await s.lib_url_parse(u_clone.episodes).await
} }
Library::LibraryType::Manga => { SourceType::Manga => {
let m: Box<dyn MangaSource + Send + Sync> = ex.toMangaSource(); let m: Box<dyn MangaSource + Send + Sync> = ex.toMangaSource();
m.lib_url_parse(u_clone.episodes).await m.lib_url_parse(u_clone.episodes).await
} }
}; };
if let Ok(o) = hs { if let Ok(o) = hs {
tx_clone_input_clone.send(o); let _ = tx_clone_input_clone.send(o);
} }
} }
}); });
@@ -531,6 +597,13 @@ impl App {
} }
} }
true true
},
KeyCode::Esc | KeyCode::Backspace => {
if state.entry_selected {
state.entry_selected = false;
return true;
}
false
} }
_ => {false} _ => {false}
} }
+6 -4
View File
@@ -6,7 +6,7 @@ pub mod animeworld;
pub mod mangaworld; pub mod mangaworld;
pub const AVAIBLEANIMESOURCE: [&str; 1] = ["animeworld"]; pub const AVAIBLEANIMESOURCE: [&str; 1] = ["animeworld"];
#[derive(Clone)] #[derive(Clone, Copy)]
pub enum SourceType { pub enum SourceType {
Anime, Manga Anime, Manga
} }
@@ -35,9 +35,10 @@ pub struct Episode {
pub section: usize, pub section: usize,
pub url: String pub url: String
} }
#[derive(Clone)]
pub struct EpisodeListItem { pub struct EpisodeListItem {
title: String, pub title: String,
url: String pub url: String
} }
pub struct Comic<S: MangaSource> { pub struct Comic<S: MangaSource> {
episodes: Vec<Chapter>, episodes: Vec<Chapter>,
@@ -98,6 +99,7 @@ pub trait AnimeSource: Send + Sync {
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>; async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>;
async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>; async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>; async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>;
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
} }
#[async_trait] #[async_trait]
@@ -142,5 +144,5 @@ pub trait MangaSource: Send + Sync {
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>; async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, Box<dyn std::error::Error + Send + Sync>>;
// async fn chapter(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>; // async fn chapter(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>;
// async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>; // async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
} }
+15 -2
View File
@@ -1,10 +1,10 @@
use std::{collections::HashMap, process::Command}; use std::{collections::HashMap, process::{Command, Stdio}};
use reqwest::get; use reqwest::get;
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use crate::source::{AnimeSource, Episode, EpisodeListItem, SourceType}; use crate::{log, source::{AnimeSource, Episode, EpisodeListItem, SourceType}};
pub struct Animeworld { pub struct Animeworld {
url: String, url: String,
@@ -133,6 +133,19 @@ impl AnimeSource for Animeworld {
} }
return Ok(hs); return Ok(hs);
} }
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(url) = Self::id_extract(&eli.url) {
if let Ok(vlc_url) =self.episode_play_url(url).await {
if !vlc_url.is_empty() {
let _ = Command::new("vlc").arg(vlc_url)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
}
}
Ok(())
}
// async fn fetch_html(url: String) -> Result<String, reqwest::Error> { // async fn fetch_html(url: String) -> Result<String, reqwest::Error> {
// get(url).await?.error_for_status()?.text().await // get(url).await?.error_for_status()?.text().await
// } // }
+3 -1
View File
@@ -175,5 +175,7 @@ impl MangaSource for Mangaworld {
} }
return Ok(hs); return Ok(hs);
} }
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(()) //TODO
}
} }