feat: switch to chillhop by default

feat: add special noheader exception for legibility of tracklists
feat: add small muted display
docs: fix macos instructions
This commit is contained in:
talwat 2025-08-21 23:56:32 +02:00
parent 6f679055ea
commit f6ec3bb1fe
8 changed files with 65 additions and 43 deletions

View File

@ -10,8 +10,8 @@ curl https://raw.githubusercontent.com/talwat/lowfi/refs/heads/main/data/chillho
## MacOS ## MacOS
```sh ```sh
mkdir -p "~/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 "~/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 ## Windows
@ -21,4 +21,4 @@ Then just put [this file](https://raw.githubusercontent.com/talwat/lowfi/refs/he
## Launching lowfi ## Launching lowfi
Once the list has been added, just launch `lowfi` with `-t chillhop`. Once the list has been added, just launch `lowfi` with `-t chillhop`.

View File

@ -229,8 +229,6 @@ impl Player {
match msg { match msg {
Message::Next | Message::Init | Message::TryAgain => { Message::Next | Message::Init | Message::TryAgain => {
player.bookmarks.set_bookmarked(false);
// We manually skipped, so we shouldn't actually wait for the song // We manually skipped, so we shouldn't actually wait for the song
// to be over until we recieve the `NewSong` signal. // to be over until we recieve the `NewSong` signal.
new = false; new = false;
@ -303,17 +301,7 @@ impl Player {
let current = player.current.load(); let current = player.current.load();
let current = current.as_ref().unwrap(); let current = current.as_ref().unwrap();
player player.bookmarks.bookmark(&&current).await?;
.bookmarks
.bookmark(
current.full_path.clone(),
if current.custom_name {
Some(current.display_name.clone())
} else {
None
},
)
.await?;
} }
Message::Quit => break, Message::Quit => break,
} }

View File

@ -5,7 +5,7 @@ use tokio::fs::{create_dir_all, File, OpenOptions};
use tokio::io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use tokio::io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::data_dir; use crate::{data_dir, tracks};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum BookmarkError { pub enum BookmarkError {
@ -41,6 +41,7 @@ impl Bookmarks {
file.read_to_string(&mut text).await?; file.read_to_string(&mut text).await?;
let lines: Vec<String> = text let lines: Vec<String> = text
.trim_start_matches("noheader")
.trim() .trim()
.lines() .lines()
.filter_map(|x| { .filter_map(|x| {
@ -60,7 +61,7 @@ impl Bookmarks {
} }
pub async fn save(&self) -> eyre::Result<(), BookmarkError> { 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; let mut lock = self.file.write().await;
lock.seek(SeekFrom::Start(0)).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. /// Bookmarks a given track with a full path and optional custom name.
/// ///
/// Returns whether the track is now bookmarked, or not. /// Returns whether the track is now bookmarked, or not.
pub async fn bookmark( pub async fn bookmark(&self, track: &tracks::Info) -> eyre::Result<(), BookmarkError> {
&self, let entry = track.to_entry();
mut entry: String,
custom: Option<String>,
) -> eyre::Result<(), BookmarkError> {
if let Some(custom) = custom {
entry.push('!');
entry.push_str(&custom);
}
let idx = self.entries.read().await.iter().position(|x| **x == entry); let idx = self.entries.read().await.iter().position(|x| **x == entry);
if let Some(idx) = idx { if let Some(idx) = idx {
@ -92,7 +85,8 @@ impl Bookmarks {
self.entries.write().await.push(entry); self.entries.write().await.push(entry);
}; };
self.set_bookmarked(idx.is_none()); self.bookmarked
.swap(idx.is_none(), std::sync::atomic::Ordering::Relaxed);
Ok(()) Ok(())
} }
@ -101,7 +95,8 @@ impl Bookmarks {
self.bookmarked.load(std::sync::atomic::Ordering::Relaxed) 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 self.bookmarked
.swap(val, std::sync::atomic::Ordering::Relaxed); .swap(val, std::sync::atomic::Ordering::Relaxed);
} }

View File

@ -60,6 +60,9 @@ impl Player {
// Start playing the new track. // Start playing the new track.
player.sink.append(track.data); 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 // Notify the background downloader that there's an empty spot
// in the buffer. // in the buffer.
Downloader::notify(&itx).await?; Downloader::notify(&itx).await?;

View File

@ -59,14 +59,17 @@ pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String {
/// This represents the main "action" bars state. /// This represents the main "action" bars state.
enum ActionBar { enum ActionBar {
/// When the app is currently displaying "paused". /// When the app is paused.
Paused(Info), Paused(Info),
/// When the app is currently displaying "playing". /// When the app is playing.
Playing(Info), Playing(Info),
/// When the app is currently displaying "loading". /// When the app is loading.
Loading(f32), Loading(f32),
/// When the app is muted.
Muted,
} }
impl ActionBar { impl ActionBar {
@ -81,6 +84,11 @@ impl ActionBar {
("loading", Some((progress, 3))) ("loading", Some((progress, 3)))
} }
Self::Muted => {
let msg = "+ to increase volume";
("muted", Some((String::from(msg), msg.len())))
}
}; };
subject.map_or_else( subject.map_or_else(
@ -104,6 +112,10 @@ pub fn action(player: &Player, current: Option<&Arc<Info>>, width: usize) -> Str
|info| { |info| {
let info = info.deref().clone(); let info = info.deref().clone();
if player.sink.volume() < 0.01 {
return ActionBar::Muted;
}
if player.sink.is_paused() { if player.sink.is_paused() {
ActionBar::Paused(info) ActionBar::Paused(info)
} else { } else {

View File

@ -114,6 +114,18 @@ lazy_static! {
} }
impl Info { 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. /// Decodes a URL string into normal UTF-8.
fn decode_url(text: &str) -> String { fn decode_url(text: &str) -> String {
// The tuple contains smart pointers, so it's not really practical to use `into()`. // The tuple contains smart pointers, so it's not really practical to use `into()`.

View File

@ -60,7 +60,6 @@ where
(String, E): Into<Error>, (String, E): Into<Error>,
E: Into<Kind>, E: Into<Kind>,
{ {
#[must_use]
fn track(self, name: impl Into<String>) -> Result<T, Error> { fn track(self, name: impl Into<String>) -> Result<T, Error> {
self.map_err(|e| { self.map_err(|e| {
let error = match e.into() { let error = match e.into() {

View File

@ -28,8 +28,12 @@ pub struct List {
pub name: String, pub name: String,
/// Just the raw file, but seperated by `/n` (newlines). /// 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<String>, lines: Vec<String>,
/// The file path which the list was read from.
#[allow(dead_code)]
pub path: Option<String>,
} }
impl List { impl List {
@ -141,7 +145,7 @@ impl List {
} }
/// Parses text into a [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<String> = text let lines: Vec<String> = text
.trim_end() .trim_end()
.lines() .lines()
@ -150,6 +154,7 @@ impl List {
Self { Self {
lines, lines,
path: path.map(|s| s.to_owned()),
name: name.to_owned(), name: name.to_owned(),
} }
} }
@ -158,21 +163,29 @@ impl List {
pub async fn load(tracks: Option<&String>) -> eyre::Result<Self> { pub async fn load(tracks: Option<&String>) -> eyre::Result<Self> {
if let Some(arg) = tracks { if let Some(arg) = tracks {
// Check if the track is in ~/.local/share/lowfi, in which case we'll load that. // 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 path = data_dir()?.join(format!("{arg}.txt"));
let name = if name.exists() { name } else { arg.into() }; 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() .file_stem()
.and_then(|x| x.to_str()) .and_then(|x| x.to_str())
.ok_or_eyre("invalid track path")?; .ok_or_eyre("invalid track path")?;
Ok(Self::new(name, &raw)) Ok(Self::new(name, raw, path.to_str()))
} else { } else {
Ok(Self::new( Ok(Self::new(
"lofigirl", "chillhop",
include_str!("../../data/lofigirl.txt"), include_str!("../../data/chillhop.txt"),
None,
)) ))
} }
} }