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 {
|
||||
pub async fn new(
|
||||
client: Client,
|
||||
config: Config,
|
||||
config_path: Option<PathBuf>,
|
||||
) -> Result<Self> {
|
||||
pub async fn new(client: Client, config: Config, config_path: Option<PathBuf>) -> Result<Self> {
|
||||
let libraries = client.views().await?;
|
||||
// Best-effort: don't fail startup if the resume endpoint hiccups.
|
||||
let resume = client.resume_items(20).await.unwrap_or_default();
|
||||
@@ -235,10 +231,7 @@ impl App {
|
||||
// the rest of the show.
|
||||
let eps = self.client.series_episodes(sid).await?;
|
||||
let start = eps.iter().position(|e| e.id == item.id).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"selected episode {} not present in series {sid}",
|
||||
item.id
|
||||
)
|
||||
anyhow::anyhow!("selected episode {} not present in series {sid}", item.id)
|
||||
})?;
|
||||
let picker = build_picker(&eps, start, &item);
|
||||
self.status = format!(
|
||||
@@ -301,7 +294,8 @@ impl App {
|
||||
self.items = self.client.items(&parent_id).await?;
|
||||
}
|
||||
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());
|
||||
} else {
|
||||
self.view = View::Libraries;
|
||||
|
||||
+1
-3
@@ -100,9 +100,7 @@ fn read_file_config(explicit: Option<&Path>) -> Result<(FileConfig, PathBuf)> {
|
||||
}
|
||||
v
|
||||
};
|
||||
let mut save_target = explicit
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or(default_target);
|
||||
let mut save_target = explicit.map(|p| p.to_path_buf()).unwrap_or(default_target);
|
||||
for path in &candidates {
|
||||
if path.exists() {
|
||||
let text = std::fs::read_to_string(path)
|
||||
|
||||
+3
-12
@@ -189,10 +189,7 @@ impl Client {
|
||||
// movies, libraries, etc. where index numbers are absent.
|
||||
("SortBy", "ParentIndexNumber,IndexNumber,SortName"),
|
||||
("SortOrder", "Ascending"),
|
||||
(
|
||||
"Fields",
|
||||
"ProductionYear,RunTimeTicks,SeriesName,UserData",
|
||||
),
|
||||
("Fields", "ProductionYear,RunTimeTicks,SeriesName,UserData"),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
@@ -214,10 +211,7 @@ impl Client {
|
||||
("Limit", limit_s.as_str()),
|
||||
("Recursive", "true"),
|
||||
("MediaTypes", "Video"),
|
||||
(
|
||||
"Fields",
|
||||
"ProductionYear,RunTimeTicks,SeriesName,UserData",
|
||||
),
|
||||
("Fields", "ProductionYear,RunTimeTicks,SeriesName,UserData"),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
@@ -237,10 +231,7 @@ impl Client {
|
||||
.headers(self.headers())
|
||||
.query(&[
|
||||
("UserId", self.user_id.as_str()),
|
||||
(
|
||||
"Fields",
|
||||
"ProductionYear,RunTimeTicks,SeriesName,UserData",
|
||||
),
|
||||
("Fields", "ProductionYear,RunTimeTicks,SeriesName,UserData"),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
|
||||
+61
-17
@@ -14,17 +14,50 @@ struct TerminalSpec {
|
||||
}
|
||||
|
||||
const TERMINALS: &[TerminalSpec] = &[
|
||||
TerminalSpec { bin: "kitty", args: &[] },
|
||||
TerminalSpec { bin: "alacritty", args: &["-e"] },
|
||||
TerminalSpec { bin: "wezterm", 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"] },
|
||||
TerminalSpec {
|
||||
bin: "kitty",
|
||||
args: &[],
|
||||
},
|
||||
TerminalSpec {
|
||||
bin: "alacritty",
|
||||
args: &["-e"],
|
||||
},
|
||||
TerminalSpec {
|
||||
bin: "wezterm",
|
||||
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> {
|
||||
@@ -61,8 +94,21 @@ fn quote_desktop(s: &str) -> String {
|
||||
c.is_whitespace()
|
||||
|| matches!(
|
||||
c,
|
||||
'"' | '\\' | '$' | '`' | '\'' | '|' | '&' | ';' | '<' | '>'
|
||||
| '(' | ')' | '#' | '~' | '*' | '?'
|
||||
'"' | '\\'
|
||||
| '$'
|
||||
| '`'
|
||||
| '\''
|
||||
| '|'
|
||||
| '&'
|
||||
| ';'
|
||||
| '<'
|
||||
| '>'
|
||||
| '('
|
||||
| ')'
|
||||
| '#'
|
||||
| '~'
|
||||
| '*'
|
||||
| '?'
|
||||
)
|
||||
});
|
||||
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 dir = dirs.data_dir().join("applications");
|
||||
fs::create_dir_all(&dir)
|
||||
.with_context(|| format!("failed to create {}", dir.display()))?;
|
||||
fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
|
||||
|
||||
let target = dir.join("emby-client.desktop");
|
||||
fs::write(&target, content)
|
||||
.with_context(|| format!("failed to write {}", target.display()))?;
|
||||
fs::write(&target, content).with_context(|| format!("failed to write {}", target.display()))?;
|
||||
Ok(target)
|
||||
}
|
||||
+34
-16
@@ -86,10 +86,7 @@ pub fn prompt(save_target: Option<&Path>) -> Result<Config> {
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
fn run<B: ratatui::backend::Backend>(
|
||||
term: &mut Terminal<B>,
|
||||
form: &mut Form,
|
||||
) -> Result<Config> {
|
||||
fn run<B: ratatui::backend::Backend>(term: &mut Terminal<B>, form: &mut Form) -> Result<Config> {
|
||||
loop {
|
||||
term.draw(|f| draw(f, form))?;
|
||||
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(field_widget("Server", &form.server, form.focus == Field::Server, false), 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]);
|
||||
f.render_widget(
|
||||
field_widget("Server", &form.server, form.focus == Field::Server, false),
|
||||
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_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 {
|
||||
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]);
|
||||
}
|
||||
|
||||
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 {
|
||||
"*".repeat(value.chars().count())
|
||||
} else {
|
||||
@@ -237,13 +256,12 @@ pub(crate) fn field_widget<'a>(label: &'a str, value: &'a str, focused: bool, ma
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
Paragraph::new(format!("{display}{cursor}"))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
.title(format!(" {label} ")),
|
||||
)
|
||||
Paragraph::new(format!("{display}{cursor}")).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
.title(format!(" {label} ")),
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
Line::from(Span::styled(
|
||||
err.clone(),
|
||||
Style::default().fg(Color::Red),
|
||||
))
|
||||
Line::from(Span::styled(err.clone(), Style::default().fg(Color::Red)))
|
||||
} else {
|
||||
Line::from(Span::styled(
|
||||
"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::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::settings;
|
||||
|
||||
@@ -160,10 +160,18 @@ fn draw(f: &mut ratatui::Frame, app: &mut App) {
|
||||
let header_text = format!(
|
||||
" {} @ {} ",
|
||||
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![
|
||||
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::styled(app.breadcrumb_path(), Style::default().fg(Color::Gray)),
|
||||
]))
|
||||
@@ -173,20 +181,17 @@ fn draw(f: &mut ratatui::Frame, app: &mut App) {
|
||||
// List
|
||||
let title = match app.view {
|
||||
View::Libraries => "Libraries".to_string(),
|
||||
View::Items => app
|
||||
.current_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Items".into()),
|
||||
View::Items => app.current_title.clone().unwrap_or_else(|| "Items".into()),
|
||||
};
|
||||
|
||||
let items: Vec<ListItem> = app
|
||||
.current_list()
|
||||
.iter()
|
||||
.map(format_item)
|
||||
.collect();
|
||||
let items: Vec<ListItem> = app.current_list().iter().map(format_item).collect();
|
||||
|
||||
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(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
@@ -223,7 +228,12 @@ fn draw_picker(f: &mut ratatui::Frame, picker: &PlayPicker) {
|
||||
let area = centered_rect(60, 9, f.area());
|
||||
f.render_widget(Clear, area);
|
||||
let rows = [
|
||||
(PickerScope::Episode, 'E', "this episode only".to_string(), 1usize),
|
||||
(
|
||||
PickerScope::Episode,
|
||||
'E',
|
||||
"this episode only".to_string(),
|
||||
1usize,
|
||||
),
|
||||
(
|
||||
PickerScope::Season,
|
||||
'S',
|
||||
|
||||
Reference in new issue
Block a user