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
```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`.
Once the list has been added, just launch `lowfi` with `-t chillhop`.

View File

@ -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(&&current).await?;
}
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::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<String> = 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<String>,
) -> 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);
}

View File

@ -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?;

View File

@ -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<Info>>, 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 {

View File

@ -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()`.

View File

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

View File

@ -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<String>,
/// The file path which the list was read from.
#[allow(dead_code)]
pub path: Option<String>,
}
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<String> = 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<Self> {
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,
))
}
}