offline mode almost there
This commit is contained in:
158
src/main.rs
158
src/main.rs
@@ -48,6 +48,11 @@ enum View {
|
||||
page_size: usize,
|
||||
loading: bool,
|
||||
},
|
||||
OfflineLibrary {
|
||||
title: String,
|
||||
items: Vec<core::downloads::LocalEntry>,
|
||||
selected: usize,
|
||||
},
|
||||
Login,
|
||||
}
|
||||
|
||||
@@ -172,6 +177,7 @@ struct App {
|
||||
login_pass: String,
|
||||
login_focus: LoginFocus,
|
||||
login_error: Option<String>,
|
||||
offline: bool,
|
||||
|
||||
search: Option<SearchState>,
|
||||
mpv: Option<MpvSession>,
|
||||
@@ -203,19 +209,25 @@ impl Default for App {
|
||||
fn default() -> Self {
|
||||
let loaded = core::config::load_config().ok();
|
||||
|
||||
let (view_stack, config_status) = match &loaded {
|
||||
Some(cfg) if cfg.access_token.is_some() => (
|
||||
vec![View::Categories],
|
||||
format!("Connected: ({})", cfg.base_url),
|
||||
),
|
||||
Some(cfg) => (
|
||||
vec![View::Login],
|
||||
format!("Not Connected: ({})", cfg.base_url),
|
||||
),
|
||||
None => (
|
||||
vec![View::Login],
|
||||
"Missing Configuration".to_string(),
|
||||
),
|
||||
let (view_stack, config_status, offline) = match &loaded {
|
||||
Some(cfg) if cfg.access_token.is_some() => {
|
||||
let online = core::jellyfin::JellyfinClient::ping_base_url(&cfg.base_url);
|
||||
if online {
|
||||
(vec![View::Categories], format!("✅ Online: ({})", cfg.base_url), false)
|
||||
} else {
|
||||
(vec![View::Categories], format!("📴 Offline: ({})", cfg.base_url), true)
|
||||
}
|
||||
}
|
||||
Some(cfg) => {
|
||||
// no token: treat as "not connected", but still allow offline browsing
|
||||
let online = core::jellyfin::JellyfinClient::ping_base_url(&cfg.base_url);
|
||||
if online {
|
||||
(vec![View::Login], format!("Not Connected: ({})", cfg.base_url), false)
|
||||
} else {
|
||||
(vec![View::Categories], format!("📴 Offline (no token): ({})", cfg.base_url), true)
|
||||
}
|
||||
}
|
||||
None => (vec![View::Login], "Missing Configuration".to_string(), false),
|
||||
};
|
||||
|
||||
Self {
|
||||
@@ -225,19 +237,27 @@ impl Default for App {
|
||||
config: loaded,
|
||||
|
||||
view_stack,
|
||||
offline,
|
||||
|
||||
items: vec![
|
||||
"Shows".into(),
|
||||
"Movies".into(),
|
||||
"Music".into(),
|
||||
"Playlists".into(),
|
||||
"Collections".into(),
|
||||
"Libraries".into(),
|
||||
"Continue Watching".into(),
|
||||
"Recently Added".into(),
|
||||
"Downloads".into(),
|
||||
"Settings".into(),
|
||||
],
|
||||
items: if offline {
|
||||
vec![
|
||||
"Downloaded".into(),
|
||||
"Settings".into(),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
"Shows".into(),
|
||||
"Movies".into(),
|
||||
"Music".into(),
|
||||
"Playlists".into(),
|
||||
"Collections".into(),
|
||||
"Libraries".into(),
|
||||
"Continue Watching".into(),
|
||||
"Recently Added".into(),
|
||||
"Downloaded".into(),
|
||||
"Settings".into(),
|
||||
]
|
||||
},
|
||||
selected: 0,
|
||||
|
||||
login_user: String::new(),
|
||||
@@ -261,6 +281,7 @@ impl Default for App {
|
||||
downloads_selected: 0,
|
||||
downloaded_ids: downloads::scan_downloaded_ids().unwrap_or_default(),
|
||||
last_download_message: None,
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,6 +709,9 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
||||
View::Library { items, selected, .. } if !items.is_empty() => {
|
||||
(items[*selected].id.clone(), items[*selected].name.clone())
|
||||
}
|
||||
View::OfflineLibrary { items, selected, .. } if !items.is_empty() => {
|
||||
(items[*selected].item_id.clone(), items[*selected].title.clone())
|
||||
}
|
||||
_ => { app.status = "Nothing to play here".into(); return; }
|
||||
};
|
||||
|
||||
@@ -719,6 +743,10 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
||||
return;
|
||||
}
|
||||
KeyCode::Char('P') => {
|
||||
if matches!(app.current_view(), View::OfflineLibrary { .. }) {
|
||||
app.status = "Offline: autoplay disabled (use 'p')".into();
|
||||
return;
|
||||
}
|
||||
let cfg = match &app.config { Some(c) => c.clone(), None => { app.status="No config".into(); return; } };
|
||||
|
||||
let (item_id, item_name, selected, len) = match app.current_view() {
|
||||
@@ -968,6 +996,23 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
||||
View::Categories => {
|
||||
let cat = app.items[app.selected].clone();
|
||||
|
||||
// OFFLINE: local-only library
|
||||
if app.offline {
|
||||
if cat == "Downloaded" {
|
||||
let items = crate::core::downloads::scan_local_entries();
|
||||
|
||||
app.view_stack.push(View::OfflineLibrary {
|
||||
title: "Downloaded".into(),
|
||||
items,
|
||||
selected: 0,
|
||||
});
|
||||
app.status = "$".into();
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else offline can remain as-is (Downloads/Settings etc)
|
||||
}
|
||||
|
||||
let cfg = match &app.config {
|
||||
Some(c) => c.clone(),
|
||||
None => { app.status = "No config loaded".into(); return; }
|
||||
@@ -1182,6 +1227,11 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
||||
|
||||
maybe_prefetch(app);
|
||||
}
|
||||
View::OfflineLibrary { items, selected, .. } => {
|
||||
if items.is_empty() { return; }
|
||||
if *selected == 0 { *selected = items.len() - 1; }
|
||||
else { *selected -= 1; }
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1217,6 +1267,11 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
||||
|
||||
maybe_prefetch(app);
|
||||
}
|
||||
View::OfflineLibrary { items, selected, .. } => {
|
||||
if items.is_empty() { return; }
|
||||
*selected = (*selected + 1) % items.len();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1528,6 +1583,9 @@ fn ui(frame: &mut Frame, app: &App) {
|
||||
// Main
|
||||
|
||||
match app.current_view() {
|
||||
View::OfflineLibrary { title, items, selected } => {
|
||||
render_offline_library(frame, title, items, *selected, panes.left_main, panes.right_main);
|
||||
}
|
||||
View::Categories => {
|
||||
render_categories(frame, app, panes.left_main, panes.right_main);
|
||||
}
|
||||
@@ -2985,4 +3043,52 @@ fn render_login(frame: &mut Frame, app: &App, left: Rect, right: Rect) {
|
||||
.block(Block::bordered().title("Info").merge_borders(MergeStrategy::Exact)),
|
||||
right,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_offline_library(
|
||||
frame: &mut Frame,
|
||||
title: &str,
|
||||
items: &[core::downloads::LocalEntry],
|
||||
selected: usize,
|
||||
left: Rect,
|
||||
right: Rect,
|
||||
) {
|
||||
let viewport = left.height.saturating_sub(2) as usize;
|
||||
let len = items.len();
|
||||
let selected = selected.min(len.saturating_sub(1));
|
||||
|
||||
let (start, end) = compute_window(len, selected, viewport);
|
||||
|
||||
let list_items: Vec<ListItem> = items[start..end]
|
||||
.iter()
|
||||
.map(|it| ListItem::new(format!("💾 {}", it.title)))
|
||||
.collect();
|
||||
|
||||
let list = List::new(list_items)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title(format!("{title} [{}/{}]", items.len(), items.len()))
|
||||
.merge_borders(MergeStrategy::Exact),
|
||||
)
|
||||
.highlight_symbol("> ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
|
||||
let mut state = ListState::default();
|
||||
state.select(if len > 0 { Some(selected - start) } else { None });
|
||||
|
||||
frame.render_stateful_widget(list, left, &mut state);
|
||||
|
||||
let detail = if len > 0 {
|
||||
let it = &items[selected];
|
||||
format!("Title: {}\nItemId: {}\nPath:\n{}", it.title, it.item_id, it.path.display())
|
||||
} else {
|
||||
"No downloaded items found".to_string()
|
||||
};
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(detail)
|
||||
.wrap(Wrap { trim: false })
|
||||
.block(Block::bordered().title("Details").merge_borders(MergeStrategy::Exact)),
|
||||
right,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user