From f6ec3bb1fe4afa93317a449b153e0edb45072de6 Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Thu, 21 Aug 2025 23:56:32 +0200 Subject: [PATCH] feat: switch to chillhop by default feat: add special noheader exception for legibility of tracklists feat: add small muted display docs: fix macos instructions --- CHILLHOP.md | 6 +++--- src/player.rs | 14 +------------- src/player/bookmark.rs | 23 +++++++++-------------- src/player/queue.rs | 3 +++ src/player/ui/components.rs | 18 +++++++++++++++--- src/tracks.rs | 12 ++++++++++++ src/tracks/error.rs | 1 - src/tracks/list.rs | 31 ++++++++++++++++++++++--------- 8 files changed, 65 insertions(+), 43 deletions(-) diff --git a/CHILLHOP.md b/CHILLHOP.md index 303cba8..6aebf22 100644 --- a/CHILLHOP.md +++ b/CHILLHOP.md @@ -10,8 +10,8 @@ curl https://raw.githubusercontent.com/talwat/lowfi/refs/heads/main/data/chillho ## MacOS ```sh -mkdir -p "~/Library/Application Support/lowfi" -curl https://raw.githubusercontent.com/talwat/lowfi/refs/heads/main/data/chillhop.txt -O --output-dir "~/Library/Application Support/lowfi" +mkdir -p "$HOME/Library/Application Support/lowfi" +curl https://raw.githubusercontent.com/talwat/lowfi/refs/heads/main/data/chillhop.txt -O --output-dir "$HOME/Library/Application Support/lowfi" ``` ## Windows @@ -21,4 +21,4 @@ Then just put [this file](https://raw.githubusercontent.com/talwat/lowfi/refs/he ## Launching lowfi -Once the list has been added, just launch `lowfi` with `-t chillhop`. \ No newline at end of file +Once the list has been added, just launch `lowfi` with `-t chillhop`. diff --git a/src/player.rs b/src/player.rs index 973efbd..c6ce22f 100644 --- a/src/player.rs +++ b/src/player.rs @@ -229,8 +229,6 @@ impl Player { match msg { Message::Next | Message::Init | Message::TryAgain => { - player.bookmarks.set_bookmarked(false); - // We manually skipped, so we shouldn't actually wait for the song // to be over until we recieve the `NewSong` signal. new = false; @@ -303,17 +301,7 @@ impl Player { let current = player.current.load(); let current = current.as_ref().unwrap(); - player - .bookmarks - .bookmark( - current.full_path.clone(), - if current.custom_name { - Some(current.display_name.clone()) - } else { - None - }, - ) - .await?; + player.bookmarks.bookmark(&¤t).await?; } Message::Quit => break, } diff --git a/src/player/bookmark.rs b/src/player/bookmark.rs index c3012ee..c71b7a4 100644 --- a/src/player/bookmark.rs +++ b/src/player/bookmark.rs @@ -5,7 +5,7 @@ use tokio::fs::{create_dir_all, File, OpenOptions}; use tokio::io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use tokio::sync::RwLock; -use crate::data_dir; +use crate::{data_dir, tracks}; #[derive(Debug, thiserror::Error)] pub enum BookmarkError { @@ -41,6 +41,7 @@ impl Bookmarks { file.read_to_string(&mut text).await?; let lines: Vec = text + .trim_start_matches("noheader") .trim() .lines() .filter_map(|x| { @@ -60,7 +61,7 @@ impl Bookmarks { } pub async fn save(&self) -> eyre::Result<(), BookmarkError> { - let text = format!("\n{}", self.entries.read().await.join("\n")); + let text = format!("noheader\n{}", self.entries.read().await.join("\n")); let mut lock = self.file.write().await; lock.seek(SeekFrom::Start(0)).await?; @@ -74,16 +75,8 @@ impl Bookmarks { /// Bookmarks a given track with a full path and optional custom name. /// /// Returns whether the track is now bookmarked, or not. - pub async fn bookmark( - &self, - mut entry: String, - custom: Option, - ) -> eyre::Result<(), BookmarkError> { - if let Some(custom) = custom { - entry.push('!'); - entry.push_str(&custom); - } - + pub async fn bookmark(&self, track: &tracks::Info) -> eyre::Result<(), BookmarkError> { + let entry = track.to_entry(); let idx = self.entries.read().await.iter().position(|x| **x == entry); if let Some(idx) = idx { @@ -92,7 +85,8 @@ impl Bookmarks { self.entries.write().await.push(entry); }; - self.set_bookmarked(idx.is_none()); + self.bookmarked + .swap(idx.is_none(), std::sync::atomic::Ordering::Relaxed); Ok(()) } @@ -101,7 +95,8 @@ impl Bookmarks { self.bookmarked.load(std::sync::atomic::Ordering::Relaxed) } - pub fn set_bookmarked(&self, val: bool) { + pub async fn set_bookmarked(&self, track: &tracks::Info) { + let val = self.entries.read().await.contains(&track.to_entry()); self.bookmarked .swap(val, std::sync::atomic::Ordering::Relaxed); } diff --git a/src/player/queue.rs b/src/player/queue.rs index 1db5c33..6997b0b 100644 --- a/src/player/queue.rs +++ b/src/player/queue.rs @@ -60,6 +60,9 @@ impl Player { // Start playing the new track. player.sink.append(track.data); + // Set whether it's bookmarked. + player.bookmarks.set_bookmarked(&track.info).await; + // Notify the background downloader that there's an empty spot // in the buffer. Downloader::notify(&itx).await?; diff --git a/src/player/ui/components.rs b/src/player/ui/components.rs index 63a2241..41cc730 100644 --- a/src/player/ui/components.rs +++ b/src/player/ui/components.rs @@ -59,14 +59,17 @@ pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String { /// This represents the main "action" bars state. enum ActionBar { - /// When the app is currently displaying "paused". + /// When the app is paused. Paused(Info), - /// When the app is currently displaying "playing". + /// When the app is playing. Playing(Info), - /// When the app is currently displaying "loading". + /// When the app is loading. Loading(f32), + + /// When the app is muted. + Muted, } impl ActionBar { @@ -81,6 +84,11 @@ impl ActionBar { ("loading", Some((progress, 3))) } + Self::Muted => { + let msg = "+ to increase volume"; + + ("muted", Some((String::from(msg), msg.len()))) + } }; subject.map_or_else( @@ -104,6 +112,10 @@ pub fn action(player: &Player, current: Option<&Arc>, width: usize) -> Str |info| { let info = info.deref().clone(); + if player.sink.volume() < 0.01 { + return ActionBar::Muted; + } + if player.sink.is_paused() { ActionBar::Paused(info) } else { diff --git a/src/tracks.rs b/src/tracks.rs index acc2c7b..78aad75 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -114,6 +114,18 @@ lazy_static! { } impl Info { + /// Converts the info back into a full track list entry. + pub fn to_entry(&self) -> String { + let mut entry = self.full_path.clone(); + + if self.custom_name { + entry.push('!'); + entry.push_str(&self.display_name); + } + + entry + } + /// Decodes a URL string into normal UTF-8. fn decode_url(text: &str) -> String { // The tuple contains smart pointers, so it's not really practical to use `into()`. diff --git a/src/tracks/error.rs b/src/tracks/error.rs index c2748a0..1f763d1 100644 --- a/src/tracks/error.rs +++ b/src/tracks/error.rs @@ -60,7 +60,6 @@ where (String, E): Into, E: Into, { - #[must_use] fn track(self, name: impl Into) -> Result { self.map_err(|e| { let error = match e.into() { diff --git a/src/tracks/list.rs b/src/tracks/list.rs index b7b0471..2913438 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -28,8 +28,12 @@ pub struct List { pub name: String, /// Just the raw file, but seperated by `/n` (newlines). - /// `lines[0]` is the base, with the rest being tracks. + /// `lines[0]` is the base/heaeder, with the rest being tracks. lines: Vec, + + /// The file path which the list was read from. + #[allow(dead_code)] + pub path: Option, } impl List { @@ -141,7 +145,7 @@ impl List { } /// Parses text into a [List]. - pub fn new(name: &str, text: &str) -> Self { + pub fn new(name: &str, text: &str, path: Option<&str>) -> Self { let lines: Vec = text .trim_end() .lines() @@ -150,6 +154,7 @@ impl List { Self { lines, + path: path.map(|s| s.to_owned()), name: name.to_owned(), } } @@ -158,21 +163,29 @@ impl List { pub async fn load(tracks: Option<&String>) -> eyre::Result { if let Some(arg) = tracks { // Check if the track is in ~/.local/share/lowfi, in which case we'll load that. - let name = data_dir()?.join(format!("{arg}.txt")); - let name = if name.exists() { name } else { arg.into() }; + let path = data_dir()?.join(format!("{arg}.txt")); + let path = if path.exists() { path } else { arg.into() }; - let raw = fs::read_to_string(name.clone()).await?; + let raw = fs::read_to_string(path.clone()).await?; - let name = name + // Get rid of special noheader case for tracklists without a header. + let raw = if let Some(stripped) = raw.strip_prefix("noheader") { + stripped + } else { + &raw + }; + + let name = path .file_stem() .and_then(|x| x.to_str()) .ok_or_eyre("invalid track path")?; - Ok(Self::new(name, &raw)) + Ok(Self::new(name, raw, path.to_str())) } else { Ok(Self::new( - "lofigirl", - include_str!("../../data/lofigirl.txt"), + "chillhop", + include_str!("../../data/chillhop.txt"), + None, )) } }