mirror of
https://github.com/talwat/lowfi
synced 2025-08-23 10:03:56 +00:00
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:
parent
6f679055ea
commit
f6ec3bb1fe
@ -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`.
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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?;
|
||||
|
@ -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 {
|
||||
|
@ -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()`.
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user