1 parent
8c0986161a
commit
d4cea55075
7 files changed
+128
-76
No files matched your search
+4
-10
@@ -94,11 +94,7 @@ pub struct App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub async fn new(
|
pub async fn new(client: Client, config: Config, config_path: Option<PathBuf>) -> Result<Self> {
|
||||||
client: Client,
|
|
||||||
config: Config,
|
|
||||||
config_path: Option<PathBuf>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let libraries = client.views().await?;
|
let libraries = client.views().await?;
|
||||||
// Best-effort: don't fail startup if the resume endpoint hiccups.
|
// Best-effort: don't fail startup if the resume endpoint hiccups.
|
||||||
let resume = client.resume_items(20).await.unwrap_or_default();
|
let resume = client.resume_items(20).await.unwrap_or_default();
|
||||||
@@ -235,10 +231,7 @@ impl App {
|
|||||||
// the rest of the show.
|
// the rest of the show.
|
||||||
let eps = self.client.series_episodes(sid).await?;
|
let eps = self.client.series_episodes(sid).await?;
|
||||||
let start = eps.iter().position(|e| e.id == item.id).ok_or_else(|| {
|
let start = eps.iter().position(|e| e.id == item.id).ok_or_else(|| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!("selected episode {} not present in series {sid}", item.id)
|
||||||
"selected episode {} not present in series {sid}",
|
|
||||||
item.id
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
let picker = build_picker(&eps, start, &item);
|
let picker = build_picker(&eps, start, &item);
|
||||||
self.status = format!(
|
self.status = format!(
|
||||||
@@ -301,7 +294,8 @@ impl App {
|
|||||||
self.items = self.client.items(&parent_id).await?;
|
self.items = self.client.items(&parent_id).await?;
|
||||||
}
|
}
|
||||||
self.current_title = Some(parent_name.clone());
|
self.current_title = Some(parent_name.clone());
|
||||||
self.list_state.select(restore_selection(restore, self.items.len()));
|
self.list_state
|
||||||
|
.select(restore_selection(restore, self.items.len()));
|
||||||
self.status = format!("{}: {} items", parent_name, self.items.len());
|
self.status = format!("{}: {} items", parent_name, self.items.len());
|
||||||
} else {
|
} else {
|
||||||
self.view = View::Libraries;
|
self.view = View::Libraries;
|
||||||
|
|||||||
+1
-3
@@ -100,9 +100,7 @@ fn read_file_config(explicit: Option<&Path>) -> Result<(FileConfig, PathBuf)> {
|
|||||||
}
|
}
|
||||||
v
|
v
|
||||||
};
|
};
|
||||||
let mut save_target = explicit
|
let mut save_target = explicit.map(|p| p.to_path_buf()).unwrap_or(default_target);
|
||||||
.map(|p| p.to_path_buf())
|
|
||||||
.unwrap_or(default_target);
|
|
||||||
for path in &candidates {
|
for path in &candidates {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let text = std::fs::read_to_string(path)
|
let text = std::fs::read_to_string(path)
|
||||||
|
|||||||
+3
-12
@@ -189,10 +189,7 @@ impl Client {
|
|||||||
// movies, libraries, etc. where index numbers are absent.
|
// movies, libraries, etc. where index numbers are absent.
|
||||||
("SortBy", "ParentIndexNumber,IndexNumber,SortName"),
|
("SortBy", "ParentIndexNumber,IndexNumber,SortName"),
|
||||||
("SortOrder", "Ascending"),
|
("SortOrder", "Ascending"),
|
||||||
(
|
("Fields", "ProductionYear,RunTimeTicks,SeriesName,UserData"),
|
||||||
"Fields",
|
|
||||||
"ProductionYear,RunTimeTicks,SeriesName,UserData",
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
@@ -214,10 +211,7 @@ impl Client {
|
|||||||
("Limit", limit_s.as_str()),
|
("Limit", limit_s.as_str()),
|
||||||
("Recursive", "true"),
|
("Recursive", "true"),
|
||||||
("MediaTypes", "Video"),
|
("MediaTypes", "Video"),
|
||||||
(
|
("Fields", "ProductionYear,RunTimeTicks,SeriesName,UserData"),
|
||||||
"Fields",
|
|
||||||
"ProductionYear,RunTimeTicks,SeriesName,UserData",
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
@@ -237,10 +231,7 @@ impl Client {
|
|||||||
.headers(self.headers())
|
.headers(self.headers())
|
||||||
.query(&[
|
.query(&[
|
||||||
("UserId", self.user_id.as_str()),
|
("UserId", self.user_id.as_str()),
|
||||||
(
|
("Fields", "ProductionYear,RunTimeTicks,SeriesName,UserData"),
|
||||||
"Fields",
|
|
||||||
"ProductionYear,RunTimeTicks,SeriesName,UserData",
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
+61
-17
@@ -14,17 +14,50 @@ struct TerminalSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TERMINALS: &[TerminalSpec] = &[
|
const TERMINALS: &[TerminalSpec] = &[
|
||||||
TerminalSpec { bin: "kitty", args: &[] },
|
TerminalSpec {
|
||||||
TerminalSpec { bin: "alacritty", args: &["-e"] },
|
bin: "kitty",
|
||||||
TerminalSpec { bin: "wezterm", args: &["start", "--"] },
|
args: &[],
|
||||||
TerminalSpec { bin: "foot", args: &[] },
|
},
|
||||||
TerminalSpec { bin: "ghostty", args: &["-e"] },
|
TerminalSpec {
|
||||||
TerminalSpec { bin: "gnome-terminal", args: &["--"] },
|
bin: "alacritty",
|
||||||
TerminalSpec { bin: "konsole", args: &["-e"] },
|
args: &["-e"],
|
||||||
TerminalSpec { bin: "xfce4-terminal", args: &["-x"] },
|
},
|
||||||
TerminalSpec { bin: "tilix", args: &["-e"] },
|
TerminalSpec {
|
||||||
TerminalSpec { bin: "x-terminal-emulator", args: &["-e"] },
|
bin: "wezterm",
|
||||||
TerminalSpec { bin: "xterm", args: &["-e"] },
|
args: &["start", "--"],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "foot",
|
||||||
|
args: &[],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "ghostty",
|
||||||
|
args: &["-e"],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "gnome-terminal",
|
||||||
|
args: &["--"],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "konsole",
|
||||||
|
args: &["-e"],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "xfce4-terminal",
|
||||||
|
args: &["-x"],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "tilix",
|
||||||
|
args: &["-e"],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "x-terminal-emulator",
|
||||||
|
args: &["-e"],
|
||||||
|
},
|
||||||
|
TerminalSpec {
|
||||||
|
bin: "xterm",
|
||||||
|
args: &["-e"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
fn which(bin: &str) -> Option<PathBuf> {
|
fn which(bin: &str) -> Option<PathBuf> {
|
||||||
@@ -61,8 +94,21 @@ fn quote_desktop(s: &str) -> String {
|
|||||||
c.is_whitespace()
|
c.is_whitespace()
|
||||||
|| matches!(
|
|| matches!(
|
||||||
c,
|
c,
|
||||||
'"' | '\\' | '$' | '`' | '\'' | '|' | '&' | ';' | '<' | '>'
|
'"' | '\\'
|
||||||
| '(' | ')' | '#' | '~' | '*' | '?'
|
| '$'
|
||||||
|
| '`'
|
||||||
|
| '\''
|
||||||
|
| '|'
|
||||||
|
| '&'
|
||||||
|
| ';'
|
||||||
|
| '<'
|
||||||
|
| '>'
|
||||||
|
| '('
|
||||||
|
| ')'
|
||||||
|
| '#'
|
||||||
|
| '~'
|
||||||
|
| '*'
|
||||||
|
| '?'
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
if !needs_quote {
|
if !needs_quote {
|
||||||
@@ -107,11 +153,9 @@ pub fn install_desktop_entry() -> Result<PathBuf> {
|
|||||||
|
|
||||||
let dirs = BaseDirs::new().context("could not determine user data directory")?;
|
let dirs = BaseDirs::new().context("could not determine user data directory")?;
|
||||||
let dir = dirs.data_dir().join("applications");
|
let dir = dirs.data_dir().join("applications");
|
||||||
fs::create_dir_all(&dir)
|
fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
|
||||||
.with_context(|| format!("failed to create {}", dir.display()))?;
|
|
||||||
|
|
||||||
let target = dir.join("emby-client.desktop");
|
let target = dir.join("emby-client.desktop");
|
||||||
fs::write(&target, content)
|
fs::write(&target, content).with_context(|| format!("failed to write {}", target.display()))?;
|
||||||
.with_context(|| format!("failed to write {}", target.display()))?;
|
|
||||||
Ok(target)
|
Ok(target)
|
||||||
}
|
}
|
||||||
+34
-16
@@ -86,10 +86,7 @@ pub fn prompt(save_target: Option<&Path>) -> Result<Config> {
|
|||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run<B: ratatui::backend::Backend>(
|
fn run<B: ratatui::backend::Backend>(term: &mut Terminal<B>, form: &mut Form) -> Result<Config> {
|
||||||
term: &mut Terminal<B>,
|
|
||||||
form: &mut Form,
|
|
||||||
) -> Result<Config> {
|
|
||||||
loop {
|
loop {
|
||||||
term.draw(|f| draw(f, form))?;
|
term.draw(|f| draw(f, form))?;
|
||||||
if !event::poll(Duration::from_millis(200))? {
|
if !event::poll(Duration::from_millis(200))? {
|
||||||
@@ -198,13 +195,30 @@ fn draw(f: &mut ratatui::Frame, form: &Form) {
|
|||||||
));
|
));
|
||||||
f.render_widget(hint, rows[0]);
|
f.render_widget(hint, rows[0]);
|
||||||
|
|
||||||
f.render_widget(field_widget("Server", &form.server, form.focus == Field::Server, false), rows[1]);
|
f.render_widget(
|
||||||
f.render_widget(field_widget("User", &form.user, form.focus == Field::User, false), rows[2]);
|
field_widget("Server", &form.server, form.focus == Field::Server, false),
|
||||||
f.render_widget(field_widget("Password", &form.password, form.focus == Field::Password, true), rows[3]);
|
rows[1],
|
||||||
|
);
|
||||||
|
f.render_widget(
|
||||||
|
field_widget("User", &form.user, form.focus == Field::User, false),
|
||||||
|
rows[2],
|
||||||
|
);
|
||||||
|
f.render_widget(
|
||||||
|
field_widget(
|
||||||
|
"Password",
|
||||||
|
&form.password,
|
||||||
|
form.focus == Field::Password,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
rows[3],
|
||||||
|
);
|
||||||
|
|
||||||
let toggle_marker = if form.save_to_disk { "[x]" } else { "[ ]" };
|
let toggle_marker = if form.save_to_disk { "[x]" } else { "[ ]" };
|
||||||
let toggle_style = if form.focus == Field::SaveToggle {
|
let toggle_style = if form.focus == Field::SaveToggle {
|
||||||
Style::default().fg(Color::Black).bg(Color::Yellow).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.fg(Color::Black)
|
||||||
|
.bg(Color::Yellow)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::White)
|
Style::default().fg(Color::White)
|
||||||
};
|
};
|
||||||
@@ -225,7 +239,12 @@ fn draw(f: &mut ratatui::Frame, form: &Form) {
|
|||||||
f.render_widget(Paragraph::new(footer), rows[5]);
|
f.render_widget(Paragraph::new(footer), rows[5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn field_widget<'a>(label: &'a str, value: &'a str, focused: bool, mask: bool) -> Paragraph<'a> {
|
pub(crate) fn field_widget<'a>(
|
||||||
|
label: &'a str,
|
||||||
|
value: &'a str,
|
||||||
|
focused: bool,
|
||||||
|
mask: bool,
|
||||||
|
) -> Paragraph<'a> {
|
||||||
let display: String = if mask {
|
let display: String = if mask {
|
||||||
"*".repeat(value.chars().count())
|
"*".repeat(value.chars().count())
|
||||||
} else {
|
} else {
|
||||||
@@ -237,13 +256,12 @@ pub(crate) fn field_widget<'a>(label: &'a str, value: &'a str, focused: bool, ma
|
|||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(Color::DarkGray)
|
||||||
};
|
};
|
||||||
Paragraph::new(format!("{display}{cursor}"))
|
Paragraph::new(format!("{display}{cursor}")).block(
|
||||||
.block(
|
Block::default()
|
||||||
Block::default()
|
.borders(Borders::ALL)
|
||||||
.borders(Borders::ALL)
|
.border_style(border_style)
|
||||||
.border_style(border_style)
|
.title(format!(" {label} ")),
|
||||||
.title(format!(" {label} ")),
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
pub(crate) fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||||
|
|||||||
+1
-4
@@ -271,10 +271,7 @@ fn draw(f: &mut ratatui::Frame, form: &Form) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let footer = if let Some(err) = &form.error {
|
let footer = if let Some(err) = &form.error {
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(err.clone(), Style::default().fg(Color::Red)))
|
||||||
err.clone(),
|
|
||||||
Style::default().fg(Color::Red),
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
"Tab/↓ next · Shift+Tab/↑ prev · Enter save · Esc cancel",
|
"Tab/↓ next · Shift+Tab/↑ prev · Enter save · Esc cancel",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph};
|
|||||||
use std::io::{Stdout, stdout};
|
use std::io::{Stdout, stdout};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::app::{App, Action, PickerScope, PlayPicker, RESUME_VIRTUAL_ID, View};
|
use crate::app::{Action, App, PickerScope, PlayPicker, RESUME_VIRTUAL_ID, View};
|
||||||
use crate::emby::Item;
|
use crate::emby::Item;
|
||||||
use crate::settings;
|
use crate::settings;
|
||||||
|
|
||||||
@@ -160,10 +160,18 @@ fn draw(f: &mut ratatui::Frame, app: &mut App) {
|
|||||||
let header_text = format!(
|
let header_text = format!(
|
||||||
" {} @ {} ",
|
" {} @ {} ",
|
||||||
app.client.user_name,
|
app.client.user_name,
|
||||||
app.client.server.trim_start_matches("http://").trim_start_matches("https://"),
|
app.client
|
||||||
|
.server
|
||||||
|
.trim_start_matches("http://")
|
||||||
|
.trim_start_matches("https://"),
|
||||||
);
|
);
|
||||||
let header = Paragraph::new(Line::from(vec![
|
let header = Paragraph::new(Line::from(vec![
|
||||||
Span::styled(header_text, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
|
Span::styled(
|
||||||
|
header_text,
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(app.breadcrumb_path(), Style::default().fg(Color::Gray)),
|
Span::styled(app.breadcrumb_path(), Style::default().fg(Color::Gray)),
|
||||||
]))
|
]))
|
||||||
@@ -173,20 +181,17 @@ fn draw(f: &mut ratatui::Frame, app: &mut App) {
|
|||||||
// List
|
// List
|
||||||
let title = match app.view {
|
let title = match app.view {
|
||||||
View::Libraries => "Libraries".to_string(),
|
View::Libraries => "Libraries".to_string(),
|
||||||
View::Items => app
|
View::Items => app.current_title.clone().unwrap_or_else(|| "Items".into()),
|
||||||
.current_title
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "Items".into()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let items: Vec<ListItem> = app
|
let items: Vec<ListItem> = app.current_list().iter().map(format_item).collect();
|
||||||
.current_list()
|
|
||||||
.iter()
|
|
||||||
.map(format_item)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let list = List::new(items)
|
let list = List::new(items)
|
||||||
.block(Block::default().borders(Borders::ALL).title(format!(" {title} ")))
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(format!(" {title} ")),
|
||||||
|
)
|
||||||
.highlight_style(
|
.highlight_style(
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(Color::Black)
|
||||||
@@ -223,7 +228,12 @@ fn draw_picker(f: &mut ratatui::Frame, picker: &PlayPicker) {
|
|||||||
let area = centered_rect(60, 9, f.area());
|
let area = centered_rect(60, 9, f.area());
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
let rows = [
|
let rows = [
|
||||||
(PickerScope::Episode, 'E', "this episode only".to_string(), 1usize),
|
(
|
||||||
|
PickerScope::Episode,
|
||||||
|
'E',
|
||||||
|
"this episode only".to_string(),
|
||||||
|
1usize,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
PickerScope::Season,
|
PickerScope::Season,
|
||||||
'S',
|
'S',
|
||||||
|
|||||||
Reference in new issue
Block a user