use std::collections::HashSet;
use std::time::Instant;
use crate::analysis::{CopyOpts, Plan};
use crate::db::{MasterDb, SafetyOpts, rekordbox_running};
use super::data::{TrackRow, dst_visible, load_rows, src_visible};
pub const DURATION_TOL_SECS: i64 = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Focus {
Src,
Dst,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InputMode {
Normal,
Search(Focus),
Confirm,
Help,
}
#[derive(Clone, Copy, Default, Debug)]
pub struct DstFilters {
pub auto: bool,
pub fuzzy_from_src: bool,
}
#[derive(Clone, Debug)]
pub struct ColumnState {
pub query: String,
pub visible: Vec<usize>,
pub cursor: usize,
pub selected: HashSet<String>,
}
impl Default for ColumnState {
fn default() -> Self {
Self {
query: String::new(),
visible: Vec::new(),
cursor: 0,
selected: HashSet::new(),
}
}
}
impl ColumnState {
pub fn clamp_cursor(&mut self) {
if self.visible.is_empty() {
self.cursor = 0;
} else if self.cursor >= self.visible.len() {
self.cursor = self.visible.len() - 1;
}
}
pub fn move_by(&mut self, delta: isize) {
if self.visible.is_empty() {
self.cursor = 0;
return;
}
let n = self.visible.len() as isize;
let mut c = self.cursor as isize + delta;
if c < 0 {
c = 0;
}
if c >= n {
c = n - 1;
}
self.cursor = c as usize;
}
pub fn jump_top(&mut self) {
self.cursor = 0;
}
pub fn jump_bottom(&mut self) {
if !self.visible.is_empty() {
self.cursor = self.visible.len() - 1;
}
}
}
#[derive(Default)]
pub struct StatusLine {
pub text: String,
pub level: StatusLevel,
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum StatusLevel {
#[default]
Info,
Warn,
Err,
Ok,
}
impl StatusLine {
pub fn info(&mut self, msg: impl Into<String>) {
self.text = msg.into();
self.level = StatusLevel::Info;
}
pub fn ok(&mut self, msg: impl Into<String>) {
self.text = msg.into();
self.level = StatusLevel::Ok;
}
pub fn warn(&mut self, msg: impl Into<String>) {
self.text = msg.into();
self.level = StatusLevel::Warn;
}
pub fn err(&mut self, msg: impl Into<String>) {
self.text = msg.into();
self.level = StatusLevel::Err;
}
}
pub struct PendingBatch {
pub plans: Vec<Plan>,
pub failures: Vec<(String, String)>, }
pub struct App {
pub db: MasterDb,
pub safety: SafetyOpts,
pub rows: Vec<TrackRow>,
pub rb_running: bool,
pub rb_last_polled: Instant,
pub src: ColumnState,
pub dst: ColumnState,
pub focus: Focus,
pub mode: InputMode,
pub copy_opts: CopyOpts,
pub dst_filters: DstFilters,
pub status: StatusLine,
pub pending: Option<PendingBatch>,
pub unresolved_errors: bool,
pub quit_pending: bool,
pub should_quit: bool,
}
impl App {
pub fn new(db: MasterDb, safety: SafetyOpts) -> anyhow::Result<Self> {
let rows = load_rows(&db)?;
let mut app = App {
db,
safety,
rows,
rb_running: rekordbox_running(),
rb_last_polled: Instant::now(),
src: ColumnState::default(),
dst: ColumnState::default(),
focus: Focus::Src,
mode: InputMode::Normal,
copy_opts: CopyOpts::default(),
dst_filters: DstFilters::default(),
status: StatusLine::default(),
pending: None,
unresolved_errors: false,
quit_pending: false,
should_quit: false,
};
app.recompute_visible();
app.status.info(format!("Loaded {} tracks.", app.rows.len()));
Ok(app)
}
pub fn recompute_visible(&mut self) {
self.src.visible = src_visible(&self.rows, &self.src.query);
self.src.clamp_cursor();
let src = self
.src
.visible
.get(self.src.cursor)
.and_then(|&i| self.rows.get(i))
.cloned();
self.dst.visible = dst_visible(
&self.rows,
&self.dst.query,
self.dst_filters.auto,
src.as_ref(),
self.dst_filters.fuzzy_from_src,
DURATION_TOL_SECS,
);
self.dst.clamp_cursor();
}
pub fn reload_db(&mut self) -> anyhow::Result<()> {
self.rows = load_rows(&self.db)?;
let existing: HashSet<&str> = self.rows.iter().map(|r| r.id.as_str()).collect();
self.dst.selected.retain(|id| existing.contains(id.as_str()));
self.recompute_visible();
Ok(())
}
pub fn poll_rekordbox_if_due(&mut self) {
if self.rb_last_polled.elapsed() >= std::time::Duration::from_secs(1) {
self.rb_running = rekordbox_running();
self.rb_last_polled = Instant::now();
}
}
pub fn focused_column_mut(&mut self) -> &mut ColumnState {
match self.focus {
Focus::Src => &mut self.src,
Focus::Dst => &mut self.dst,
}
}
pub fn focused_column(&self) -> &ColumnState {
match self.focus {
Focus::Src => &self.src,
Focus::Dst => &self.dst,
}
}
pub fn current_src(&self) -> Option<&TrackRow> {
self.src
.visible
.get(self.src.cursor)
.and_then(|&i| self.rows.get(i))
}
pub fn current_dst(&self) -> Option<&TrackRow> {
self.dst
.visible
.get(self.dst.cursor)
.and_then(|&i| self.rows.get(i))
}
}