mirror of
https://github.com/talwat/lowfi
synced 2025-08-26 19:40:57 +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
|
## 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
|
||||||
|
@ -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(&¤t).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,
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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?;
|
||||||
|
@ -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 {
|
||||||
|
@ -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()`.
|
||||||
|
@ -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() {
|
||||||
|
@ -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,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user