Compare commits

..

30 commits

Author SHA1 Message Date
Leah ab04b91bff
webmusic.js: prevent default action for keydown events 2021-03-13 02:16:35 +01:00
Leah 7d750777ec
webmusic.js: check for playable content 2021-03-08 14:06:06 +01:00
Leah bb01d955af
webmusic.js: hide selector when track selected 2021-03-08 13:35:35 +01:00
Isabelle 6a8708bcf8 webmusic.js: ignore keyboard controls if ctrl or alt is pressed 2021-03-07 17:53:51 +01:00
Isabelle acac39a90c webmusic.js: seeking in track with number keys 2021-03-07 17:53:46 +01:00
Leah d9bed3ac47 webmusic.js: use single quotes everywhere 2021-03-07 17:12:07 +01:00
Leah db8b55cadc webmusic.js: use arrow functions everywhere 2021-03-07 17:12:01 +01:00
Leah b59a5633f0 webmusic.js: use const for audioplayer and variable element in function playSong 2021-03-07 17:11:53 +01:00
Leah faeb30325f webmusic.js: use strict mode 2021-03-07 16:58:09 +01:00
Leah 0d789a5e11 webmusic.js: add classes needed for previous 2 commits 2021-03-07 16:48:09 +01:00
Leah e5ea274135 webmusic.js: use classes for buttons 2021-03-07 16:47:32 +01:00
Leah 6d0e9c999d webmusic.js: navigating through files/folders with arrowkeys/enter 2021-03-07 16:47:24 +01:00
Leah 155e7c1bbb webmusic.js: removed debug prints, better player handling 2021-03-07 16:10:53 +01:00
Leah 4b6e69d4c1 webmusic.js: bind to audioplayer-events just once 2021-03-07 15:48:23 +01:00
Leah cef0e6b625 webmusic.js: renamed gstate to playerState 2021-03-07 15:23:39 +01:00
Leah edd16ecb24 webmusic.js: renamed setState to setPlayerState and updateState to updatePlayerState 2021-03-07 15:17:29 +01:00
Leah b76f8c4af3 webmusic.css: replaced spaces with tabs 2021-03-07 15:08:00 +01:00
Leah 05ff2feabf needed changes on template and css to make previous commit work 2021-03-07 15:06:56 +01:00
Leah 94ed0d208d webmusic.js: refactor continuous- and repeat-button handling 2021-03-07 15:03:03 +01:00
Leah 1cf5c13e2b webmusic.js: general code improvements 2021-03-07 14:11:44 +01:00
Leah 6a9dd5b051 webmusic.js: rename variable 'sound' to 'audioPlayer' 2021-03-07 13:42:31 +01:00
Leah 599b1ef413 remove howler.core.js 2021-03-07 13:42:15 +01:00
Leah 7a20b78450 webmusic.js: replace howler with the native Audio() element 2021-03-07 13:39:05 +01:00
Leah 5b9d62eac2 webmusic.js: set onclick-event on state element just once 2021-03-07 01:25:48 +01:00
Leah 4db20d159a webmusic.js: skip folders 2021-03-07 01:14:16 +01:00
Leah 39f8df6ae9 needed changes on template to make previous commit work 2021-03-06 23:40:32 +01:00
Leah 9f90eed214 webmusic.js: navigate through files more smartly 2021-03-06 23:40:22 +01:00
Leah 822996211f webmusic.js: code improovements 2021-03-06 23:40:15 +01:00
Leah 81b67db9a1 webmusic.js: replace spaces with tabs 2021-03-06 23:40:00 +01:00
Leah e19f06a9d0 webmusic.js: replace ifs with switch 2021-03-06 23:39:55 +01:00
20 changed files with 290 additions and 526 deletions

2
.gitignore vendored
View file

@ -226,5 +226,3 @@ project.lock.json
music/
webmusic.linux.run
dotnetwarp_temp
.bearer_token

View file

@ -1,4 +1,4 @@
Be Gay, Do Crimes License
MIT License
Copyright (c) 2019 Laura Hausmann
@ -9,9 +9,6 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Be Gay
Do Crimes
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

View file

@ -1,18 +1,13 @@
@page
@using System.IO
@using System.Web
@model IndexModel
@{
if (Model.LogConditionsMet)
ViewData["Title"] = $"{Model.LogDisplay} ~ webmusic on .NET {Environment.Version}";
else
ViewData["Title"] = $"{Model.Displaypath} ~ webmusic on .NET {Environment.Version}";
ViewData["Title"] = $"webmusic on .NET {Environment.Version}";
}
@if (Model.Path.Contains("/..")) {
return;
}
@if (Model.Path.EndsWith(".lrc")) {
Layout = "Shared/_LayoutNojs";
<h3>@Model.Path</h3>
<p>@Html.Raw((await System.IO.File.ReadAllTextAsync("music" + Model.Path)).Replace("\n", "<br/>"))</p>
}
@ -21,87 +16,34 @@ else {
@Model.Displaypath <span id="state"></span> <span id="flags">[<span id="repeatButton">R</span><span id="continuousButton">C</span>]</span>
</h2>
<a class="action-muted">[..]</a>
<a href="?@IndexModel.Encode(Model.PathOneup)" id="back" class="entry-muted cfont"> Go back</a>
<a href="?@Model.PathOneup" id="back" class="entry-muted cfont"> Go back</a>
<br/>
<a class="action-muted">[--]</a>
<a href="/playlist/@(Request.QueryString).m3u" class="entry-muted cfont"> Download playlist</a>
@if (Model.LogConditionsMet) {
<br/>
<a class="action-muted">[--]</a>
<span id="log" onclick="log_playback('/log?artist=@Model.LogArtist&album=@Model.LogAlbum&url=@Model.LogUrl')" class="entry-muted cfont" style="cursor: pointer">Log playback</span>
<span class="action-muted">/</span>
<span id="copy_hash_np" onclick="copy_hash_np()" class="entry-muted cfont" style="cursor: pointer">Copy #np</span>
<span class="action-muted">/</span>
<span id="copy_journal_full" onclick="copy_journal_full()" class="entry-muted cfont" style="cursor: pointer">Copy journal (full)</span>
<span class="action-muted">/</span>
<span id="copy_journal_album" onclick="copy_journal_album()" class="entry-muted cfont" style="cursor: pointer">Copy journal (album only)</span>
}
<br/>
<br/>
@foreach (var dir in Model.Dirs) {
<li>
<a class="action" href="#">[--]</a>
<a class="dir" href="?@IndexModel.Encode(Model.Path + "/" + dir)"> @dir</a>
<a class="action" href="#">[--]</a>
<a class="dir" href="?@Model.Path/@dir"> @dir</a>
</li>
}
@foreach (var file in Model.Files) {
var jspath = IndexModel.Encode(Model.Fullpath);
var jsfile = jspath + "/" + IndexModel.Encode(file);
var basename = System.IO.Path.GetFileNameWithoutExtension(file);
var lrcfile = basename + ".lrc";
var lrcpath = System.IO.Path.Combine(Model.Fullpath, lrcfile);
<li>
<a class="action" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))" download>[DL]</a>
@if (System.IO.File.Exists(lrcpath)) {
<a class="action" href="/lyrics/?@Html.Raw(IndexModel.Encode(Model.Path + "/" + lrcfile))" target="_blank">[LRC]</a>
}
else if (Directory.GetFiles(Model.Fullpath, "*.lrc").Length != 0) {
<a class="action">[---]</a>
}
<a class="file" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))"> @file</a>
<a class="action" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))" download>[DL]</a>
@if (System.IO.File.Exists(lrcpath)) {
<a class="action" href="?@Model.Path/@lrcfile" target="_blank">[LRC]</a>
}
else if(Directory.GetFiles(@Model.Fullpath, "*.lrc").Length != 0) {
<a class="action">[---]</a>
}
<a class="file" href="@Html.Raw(IndexModel.Encode(Model.Fullpath + "/" + file))"> @file</a>
</li>
}
}
<script>
function log_playback(url) {
fetch(url).then(function(response) {
return response.text();
}).then(function(text) {
document.getElementById('log').innerText = 'Log playback (' + text + ')';
});
}
function reset_copy_labels() {
document.getElementById('copy_hash_np').innerText = 'Copy #np';
document.getElementById('copy_journal_full').innerText = 'Copy journal (full)';
document.getElementById('copy_journal_album').innerText = 'Copy journal (album only)';
}
function copy_hash_np() {
copyToClipboard('[#np](@Html.Raw(Log.NowPlayingUrl)) @Html.Raw(Model.CopyArtist) - @Html.Raw(Model.CopyAlbum)');
reset_copy_labels();
document.getElementById('copy_hash_np').innerText = 'Copy #np (Copied!)';
}
function copy_journal_full() {
copyToClipboard('[@Html.Raw(Model.CopyArtist)](' + getUrl('@Html.Raw(Model.LogArtist)') + ') - [@Html.Raw(Model.CopyAlbum)](' + getUrl('@Html.Raw(Model.LogArtist)', '@Html.Raw(Model.LogAlbum)') + ')')
reset_copy_labels();
document.getElementById('copy_journal_full').innerText = 'Copy journal (full) (Copied!)';
}
function copy_journal_album() {
copyToClipboard(', [@Html.Raw(Model.CopyAlbum)](' + getUrl('@Html.Raw(Model.LogArtist)', '@Html.Raw(Model.LogAlbum)') + ')')
reset_copy_labels();
document.getElementById('copy_journal_album').innerText = 'Copy journal (album only) (Copied!)';
}
function getUrl(artist, album) {
let url = location.protocol + '//' + location.host + '/?/' + artist;
if (album) {
url += '/' + album;
}
return url;
}
</script>

View file

@ -4,25 +4,17 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace webmusic.Pages {
public class IndexModel : PageModel {
private const string Root = "music";
public List<string> Dirs = new();
public string Displaypath = "";
public List<string> Files = new();
public string Fullpath = "";
public string Path = "";
public string PathOneup = "";
public bool LogConditionsMet;
public string LogArtist;
public string LogAlbum;
public string LogDisplay;
public string LogUrl;
public string CopyArtist;
public string CopyAlbum;
private const string Root = "music";
public List<string> Dirs = new();
public string Displaypath = "";
public List<string> Files = new();
public string Fullpath = "";
public string Path = "";
public string PathOneup = "";
public void OnGet() {
if (Request.QueryString.HasValue)
@ -32,9 +24,9 @@ namespace webmusic.Pages {
Response.Redirect("/Error");
return;
}
if (Path.EndsWith(".m3u"))
Path = Path[..^4];
Path = Path.Substring(0, Path.Length - 4);
if (Path.EndsWith(".lrc"))
return;
@ -49,79 +41,85 @@ namespace webmusic.Pages {
Files.RemoveAll(p => p.EndsWith(".m3u"));
Files.RemoveAll(p => p.EndsWith(".lrc"));
Files.RemoveAll(p => p.StartsWith("."));
Files.Sort(new NaturalSortComparer());
if (Log.StatisticsEnabled && Request.Headers["Remote-User"].Equals(Log.StatisticsUser) && Files.Any()) {
var pathparts = Displaypath.Split(System.IO.Path.DirectorySeparatorChar);
if (pathparts.Length > 2) {
CopyAlbum = pathparts[Index.FromEnd(1)].Replace("'", "\\'");
CopyArtist = pathparts[Index.FromEnd(2)].Replace("'", "\\'");;
LogAlbum = HttpUtility.UrlPathEncode(pathparts[Index.FromEnd(1)]).Replace("'", "\\'").Replace("&", "%26");
LogArtist = HttpUtility.UrlPathEncode(pathparts[Index.FromEnd(2)]).Replace("'", "\\'").Replace("&", "%26");
LogDisplay = $"{pathparts[Index.FromEnd(2)]} - {pathparts[Index.FromEnd(1)]}";
LogUrl = Request.GetEncodedUrl().Replace("'", "\\'").Replace("&", "%26");
LogConditionsMet = true;
}
}
Files.Sort(new AlphanumComparatorFast());
}
public static string Encode(string str) => str.Replace("\"", "%22").Replace("'", "%27").Replace("?", "%3F").Replace("&", "%26").Replace(" ", "%20".Replace("+", "%2B"));
public static string Encode(string str) => str.Replace("\"", "%22")
.Replace("'", "%27")
.Replace("?", "%3F")
.Replace("&", "%26")
.Replace(" ", "%20");
private class NaturalSortComparer : IComparer<string>, IDisposable {
private readonly bool _isAscending;
public NaturalSortComparer(bool inAscendingOrder = true) {
_isAscending = inAscendingOrder;
}
int IComparer<string>.Compare(string x, string y) {
if (x == y)
private class AlphanumComparatorFast : IComparer<string> {
public int Compare(string x, string y) {
var s1 = x;
if (s1 == null)
return 0;
if (!_table.TryGetValue(x!, out var x1)) {
x1 = Regex.Split(x.Replace(" ", ""), "([0-9]+)");
_table.Add(x, x1);
}
if (!(y is { } s2))
return 0;
if (!_table.TryGetValue(y!, out var y1)) {
y1 = Regex.Split(y.Replace(" ", ""), "([0-9]+)");
_table.Add(y, y1);
}
var len1 = s1.Length;
var len2 = s2.Length;
var marker1 = 0;
var marker2 = 0;
int returnVal;
// Walk through two the strings with two markers.
while (marker1 < len1 && marker2 < len2) {
var ch1 = s1[marker1];
var ch2 = s2[marker2];
for (var i = 0; i < x1.Length && i < y1.Length; i++) {
if (x1[i] != y1[i]) {
returnVal = PartCompare(x1[i], y1[i]);
return _isAscending ? returnVal : -returnVal;
// Some buffers we can build up characters in for each chunk.
var space1 = new char[len1];
var loc1 = 0;
var space2 = new char[len2];
var loc2 = 0;
// Walk through all following characters that are digits or
// characters in BOTH strings starting at the appropriate marker.
// Collect char arrays.
do {
space1[loc1++] = ch1;
marker1++;
if (marker1 < len1)
ch1 = s1[marker1];
else
break;
} while (char.IsDigit(ch1) == char.IsDigit(space1[0]));
do {
space2[loc2++] = ch2;
marker2++;
if (marker2 < len2)
ch2 = s2[marker2];
else
break;
} while (char.IsDigit(ch2) == char.IsDigit(space2[0]));
// If we have collected numbers, compare them numerically.
// Otherwise, if we have strings, compare them alphabetically.
var str1 = new string(space1);
var str2 = new string(space2);
int result;
if (char.IsDigit(space1[0]) && char.IsDigit(space2[0])) {
var thisNumericChunk = int.Parse(str1);
var thatNumericChunk = int.Parse(str2);
result = thisNumericChunk.CompareTo(thatNumericChunk);
}
else {
result = string.Compare(str1, str2, StringComparison.Ordinal);
}
if (result != 0)
return result;
}
if (y1.Length > x1.Length) {
returnVal = 1;
}
else if (x1.Length > y1.Length) {
returnVal = -1;
}
else {
returnVal = 0;
}
return _isAscending ? returnVal : -returnVal;
}
private static int PartCompare(string left, string right) {
if (!int.TryParse(left, out var x))
return string.Compare(left, right, StringComparison.Ordinal);
return !int.TryParse(right, out var y) ? string.Compare(left, right, StringComparison.Ordinal) : x.CompareTo(y);
}
private Dictionary<string, string[]> _table = new();
public void Dispose() {
_table.Clear();
_table = null;
return len1 - len2;
}
}
}
}
}

View file

@ -1,54 +0,0 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
namespace webmusic.Pages;
[ApiController, Route("/log")]
public class Log : Controller {
public const bool StatisticsEnabled = true;
public const string StatisticsUser = "zotan";
public const string NowPlayingUrl = "https://zotan.pw/np";
private const string StatisticsUrl = "https://zotan.pw/np/log";
private const string StatisticsSource = "music.ztn.sh";
private readonly string _statisticsToken = System.IO.File.Exists(".bearer_token") ? System.IO.File.ReadAllLines(".bearer_token")[0] : "";
[HttpGet]
public string Get(string artist, string album, string url) {
if (StatisticsEnabled && Request.Headers["Remote-User"].Equals(StatisticsUser)) {
var res = MakeRestRequest(new LogPlaybackRequest { Artist = artist, Title = album, Link = url, Source = StatisticsSource });
Response.StatusCode = (int)res;
return res switch {
HttpStatusCode.Created => "Logged",
HttpStatusCode.Accepted => "Skipped",
HttpStatusCode.Forbidden => "Invalid token",
_ => $"Error {Response.StatusCode} \"{res}\" occured"
};
}
Response.StatusCode = 403;
return null;
}
private HttpStatusCode MakeRestRequest(LogPlaybackRequest rq) {
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(_statisticsToken);
var json = JsonSerializer.Serialize(rq);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var response = client.PostAsync(StatisticsUrl, data).Result;
return response.StatusCode;
}
private class LogPlaybackRequest {
public string Artist { get; set; }
public string Title { get; set; }
public string Source { get; set; }
public string Link { get; set; }
}
}

View file

@ -6,6 +6,8 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>@ViewData["Title"]</title>
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono" rel="stylesheet">
<script src="/webmusic.js"></script>
<link href="/webmusic.css" rel="stylesheet"/>
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">

View file

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>@ViewData["Title"]</title>
<link href="/webmusic.css" rel="stylesheet"/>
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="container">
@RenderBody()
</div>
</body>
</html>

View file

@ -1,18 +0,0 @@
@page
@using System.IO
@model IndexModel
@{
ViewData["Title"] = $"{Model.Path.Split("/").Last()} ~ webmusic on .NET {Environment.Version}";
}
@if (Model.Path.Contains("/..")) {
return;
}
@if (!Model.Path.EndsWith(".lrc")) {
Response.Redirect("/" + Request.QueryString);
return;
}
@{
Layout = "Shared/_LayoutNojs";
}
<h3>@Model.Path</h3>
<p>@Html.Raw((await System.IO.File.ReadAllTextAsync("music" + Model.Path)).Replace("\n", "<br/>"))</p>

View file

@ -2,7 +2,7 @@
@model IndexModel
@{
Layout = null;
Response.ContentType = "text/plain; charset=utf-8";
Response.ContentType = "text/plain";
}
@if (Model.Path.Contains("..")) {
return;
@ -12,4 +12,4 @@
}
@foreach (var file in Model.Files) {
@Html.Raw(file + "\n")
}
}

View file

@ -4,11 +4,11 @@
@{
var path = HttpUtility.UrlDecode(Request.QueryString.Value.TrimStart('?'));
Layout = null;
Response.ContentType = "text/plain; charset=utf-8";
Response.ContentType = "text/plain";
}
@if (Model.Path.Contains("..")) {
return;
}
@foreach (var file in Model.Files) {
@Html.Raw("https://" + Request.Host + "/" + Model.Fullpath + "/" + file.Replace("?", "%3F") + "\n")
}
}

View file

@ -1,12 +1,20 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:63053",
"sslPort": 44395
}
},
"profiles": {
"webmusic": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "http://localhost:5004",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
}

View file

@ -1,4 +1,2 @@
## webmusic
This project allows users to browse and play music in a minimal web interface.
Contains experimental support for collecting listening statistics in conjunction with [zotan.pw-web](https://git.ztn.sh/zotan/zotan.pw-web).
This project allows users to browse and play music in a minimal web interface.

View file

@ -23,6 +23,10 @@ namespace webmusic {
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".flac"] = "audio/flac";
provider.Mappings[".m3u"] = "audio/m3u";
provider.Mappings[".opus"] = "audio/opus";
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
@ -32,11 +36,10 @@ namespace webmusic {
app.UseHsts();
}
var provider = new FileExtensionContentTypeProvider { Mappings = { [".flac"] = "audio/flac", [".m3u"] = "audio/m3u", [".opus"] = "audio/opus" } };
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });
app.UseStaticFiles(new StaticFileOptions {ContentTypeProvider = provider});
app.UseCookiePolicy();
app.UseMvc();
}
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net70</TargetFramework>
<TargetFramework>net50</TargetFramework>
<DebugType>full</DebugType>
</PropertyGroup>
@ -34,9 +34,4 @@
<Content Remove="music\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="fonts" />
</ItemGroup>
</Project>

View file

@ -1,23 +1,3 @@
/* inconsolata-regular - latin */
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 400;
src: local(''),
url('../fonts/inconsolata-v21-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/inconsolata-v21-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* ubuntu-mono-regular - latin */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
src: local(''),
url('../fonts/ubuntu-mono-v10-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('../fonts/ubuntu-mono-v10-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
body {
background-color: #161616;
}
@ -73,35 +53,3 @@ li {
.selected {
color: #8cd8cd;
}
/* Scrollbar colors */
/* Works on Firefox */
* {
scrollbar-width: thin;
scrollbar-color: grey #161616;
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 6px;
}
*::-webkit-scrollbar-track {
background: #161616;
}
*::-webkit-scrollbar-thumb {
background-color: grey;
border-radius: 20px;
}
::-moz-selection { /* Code for Firefox */
color: #161616;
background: #d8c18c;
}
::selection {
color: #161616;
background: #d8c18c;
}

View file

@ -1,282 +1,262 @@
'use strict';
const audioPlayer = new Audio();
let selectedItem = 0;
let playingItem = 0;
let playerState = 'idle';
let continuous = true;
let repeat = false;
let total = 0;
let onlyDirs = true;
const audioPlayer = new Audio();
let selectedItem = 0;
let playingItem = 0;
let playerState = 'idle';
let continuous = true;
let repeat = false;
let total = 0;
let onlyDirs = true;
const handleKeyEvent = (event) => {
if (event.ctrlKey === true || event.altKey === true || event.metaKey === true) return;
event.preventDefault();
event.stopPropagation();
if (event.ctrlKey === true || event.altKey === true) return;
event.preventDefault();
event.stopPropagation();
switch (event.key) {
case ' ':
case 'p':
if (onlyDirs !== false) return;
if (playerState === 'idle' && total !== 0) {
if (document.getElementById(playingItem).classList.contains('dir')) {
return nextTrack();
}
switch (event.key) {
case ' ':
case 'p':
if (onlyDirs !== false) return;
if (playerState == 'idle' && total !== 0) {
if (document.getElementById(playingItem).classList.contains('dir')) {
return nextTrack();
}
playSong(playingItem)
} else {
togglePlayback();
}
break;
playSong(playingItem)
} else {
togglePlayback();
}
break;
case '>':
nextTrack();
break;
case 'r':
toggleRepeat();
break;
case '<':
previousTrack();
break;
case 'c':
toggleContinue();
break;
case 'r':
toggleRepeat();
break;
case 'ArrowUp':
selectPreviousItem();
break;
case 'c':
toggleContinue();
break;
case 'ArrowDown':
selectNextItem();
break;
case 'ArrowUp':
selectPreviousItem();
break;
case 'ArrowLeft':
if (audioPlayer.currentTime < 10) {
audioPlayer.currentTime = 0;
} else {
audioPlayer.currentTime = audioPlayer.currentTime-10;
}
break;
case 'ArrowDown':
selectNextItem();
break;
case 'ArrowRight':
audioPlayer.currentTime = audioPlayer.currentTime+10;
break;
case 'ArrowLeft':
if (audioPlayer.currentTime < 10) {
audioPlayer.currentTime = 0;
} else {
audioPlayer.currentTime = audioPlayer.currentTime - 10;
}
break;
case 'Enter':
document.getElementById(selectedItem).click()
break;
case 'ArrowRight':
audioPlayer.currentTime = audioPlayer.currentTime + 10;
break;
case 'Escape':
document.getElementById('back').click();
break;
case 'Enter':
if (document.activeElement.href) {
document.activeElement.click();
} else {
document.getElementById(selectedItem).click();
}
break;
case 'Escape':
document.getElementById('back').click();
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
audioPlayer.currentTime = audioPlayer.duration / 100 * (event.key * 10);
break;
}
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
audioPlayer.currentTime = audioPlayer.duration / 100 * (event.key * 10);
break;
}
};
const initState = () => {
const dirElements = document.querySelectorAll('.dir');
const fileElements = document.querySelectorAll('.file');
let id = 0;
const dirElements = document.querySelectorAll('.dir');
const fileElements = document.querySelectorAll('.file');
let id = 0;
document.getElementById('state').addEventListener('click', togglePlayback)
document.getElementById('repeatButton').addEventListener('click', toggleRepeat)
document.getElementById('continuousButton').addEventListener('click', toggleContinue)
document.getElementById('state').addEventListener('click', togglePlayback)
document.getElementById('repeatButton').addEventListener('click', toggleRepeat)
document.getElementById('continuousButton').addEventListener('click', toggleContinue)
audioPlayer.addEventListener('canplay', () => {
audioPlayer.play();
});
audioPlayer.addEventListener('canplay', () => {
audioPlayer.play();
});
audioPlayer.addEventListener('play', () => {
setPlayerState('playing');
});
audioPlayer.addEventListener('play', () => {
setPlayerState('playing');
});
audioPlayer.addEventListener('pause', () => {
setPlayerState('paused');
});
audioPlayer.addEventListener('pause', () => {
setPlayerState('paused');
});
audioPlayer.addEventListener('error', () => {
setPlayerState('error loading track');
});
audioPlayer.addEventListener('error', () => {
setPlayerState('error loading track');
});
audioPlayer.addEventListener('ended', () => {
setPlayerState('idle');
nextTrack();
});
audioPlayer.addEventListener('ended', () => {
setPlayerState('idle');
nextTrack();
});
audioPlayer.addEventListener('timeupdate', () => {
updatePlayerState();
});
audioPlayer.addEventListener('timeupdate', () => {
updatePlayerState();
});
dirElements.forEach((element) => {
element.id = id++;
});
dirElements.forEach((element) => {
element.id = id++;
});
fileElements.forEach((element) => {
element.id = id++;
onlyDirs = false;
fileElements.forEach((element) => {
element.id = id++;
onlyDirs = false;
element.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
playSong(event.target.id);
});
});
element.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
playSong(event.target.id);
});
});
total = id;
updatePlayerState();
updateButtonState();
total = id;
updatePlayerState();
updateButtonState();
}
const setPlayerState = (state) => {
playerState = state;
playerState = state;
console.log('now in state: ' + state);
updatePlayerState();
console.log('now in state: ' + state);
updatePlayerState();
}
const updatePlayerState = () => {
let statestr = '[' + playerState;
let statestr = '[' + playerState;
if (!audioPlayer.paused) {
statestr += ' ' + formatTime(audioPlayer.currentTime) + '/' + formatTime(audioPlayer.duration);
}
if (!audioPlayer.paused) {
statestr += ' ' + formatTime(audioPlayer.currentTime) + '/' + formatTime(audioPlayer.duration);
}
statestr += ']';
document.getElementById('state').innerHTML = statestr;
statestr += ']';
document.getElementById('state').innerHTML = statestr;
}
const updateButtonState = () => {
if (repeat !== false) {
document.getElementById('repeatButton').classList.add('active');
} else {
document.getElementById('repeatButton').classList.remove('active');
}
if (repeat !== false) {
document.getElementById('repeatButton').classList.add('active');
} else {
document.getElementById('repeatButton').classList.remove('active');
}
if (continuous !== false) {
document.getElementById('continuousButton').classList.add('active');
} else {
document.getElementById('continuousButton').classList.remove('active');
}
if (continuous !== false) {
document.getElementById('continuousButton').classList.add('active');
} else {
document.getElementById('continuousButton').classList.remove('active');
}
}
const playSong = (id) => {
const element = document.getElementById(id);
const element = document.getElementById(id);
if (element === null) return;
if (element.classList.contains('dir')) return;
if (element === null) return;
if (element.classList.contains('dir')) return;
if (document.getElementsByClassName('playing').length > 0) {
document.getElementsByClassName('playing')[0].classList.remove('playing');
}
if (document.getElementsByClassName('playing').length > 0) {
document.getElementsByClassName('playing')[0].classList.remove('playing');
}
if (document.getElementsByClassName('selected').length > 0) {
document.getElementsByClassName('selected')[0].classList.remove('selected');
}
if (document.getElementsByClassName('selected').length > 0) {
document.getElementsByClassName('selected')[0].classList.remove('selected');
}
audioPlayer.pause()
audioPlayer.pause()
playingItem = element.id;
element.classList.add('playing');
playingItem = element.id;
element.classList.add('playing');
audioPlayer.src = element.href;
audioPlayer.src = element.href;
setPlayerState('loading');
audioPlayer.load();
setPlayerState('loading');
audioPlayer.load();
}
const togglePlayback = () => {
if (audioPlayer.paused) {
audioPlayer.play();
} else {
audioPlayer.pause();
}
if (audioPlayer.paused) {
audioPlayer.play();
} else {
audioPlayer.pause();
}
}
const toggleRepeat = () => {
repeat = !repeat;
continuous = !repeat;
audioPlayer.loop = repeat;
updateButtonState();
repeat = !repeat;
continuous = !repeat;
audioPlayer.loop = repeat;
updateButtonState();
}
const toggleContinue = () => {
continuous = !continuous;
updateButtonState();
continuous = !continuous;
updateButtonState();
}
const previousTrack = () => {
if (!continuous) return;
if (playingItem-- === 0) playingItem = total - 1;
if (!continuous) return;
if (playingItem-- === 0) playingItem = total-1;
if (document.getElementById(playingItem).classList.contains('dir')) {
return previousTrack();
}
if (document.getElementById(playingItem).classList.contains('dir')) {
return previousTrack();
}
playSong(playingItem);
playSong(playingItem);
}
const nextTrack = () => {
if (!continuous) return;
if (++playingItem === total) playingItem = 0;
if (!continuous) return;
if (++playingItem === total) playingItem = 0;
if (document.getElementById(playingItem).classList.contains('dir')) {
return nextTrack();
}
if (document.getElementById(playingItem).classList.contains('dir')) {
return nextTrack();
}
playSong(playingItem);
playSong(playingItem);
}
const selectPreviousItem = () => {
if (selectedItem-- === 0) selectedItem = total - 1;
updateSelectedItem();
if (selectedItem-- === 0) selectedItem = total-1;
updateSelectedItem();
}
const selectNextItem = () => {
if (selectedItem === 0 && document.getElementsByClassName('selected').length === 0) {
document.getElementById(selectedItem).classList.add('selected');
} else {
if (++selectedItem === total) selectedItem = 0;
updateSelectedItem();
}
if (++selectedItem === total) selectedItem = 0;
updateSelectedItem();
}
const updateSelectedItem = () => {
if (document.getElementsByClassName('selected').length > 0) {
document.getElementsByClassName('selected')[0].classList.remove('selected');
}
if (document.getElementsByClassName('selected').length > 0) {
document.getElementsByClassName('selected')[0].classList.remove('selected');
}
document.getElementById(selectedItem).classList.add('selected');
document.getElementById(selectedItem).classList.add('selected');
}
const formatTime = (secs) => {
secs = Math.round(secs);
const minutes = Math.floor(secs / 60) || 0;
const seconds = (secs - minutes * 60) || 0;
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
secs = Math.round(secs);
const minutes = Math.floor(secs / 60) || 0;
const seconds = (secs - minutes * 60) || 0;
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
}
document.addEventListener('DOMContentLoaded', initState);
document.addEventListener('keydown', handleKeyEvent);
function copyToClipboard(str) {
navigator.clipboard.writeText(str);
}