summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2023-10-30 23:08:45 +0100
committerRaindropsSys <raindrops@equestria.dev>2023-10-30 23:08:45 +0100
commit41c51b8bdb9c8e9fa4a7d56f260d594739d4107e (patch)
tree4bb3e824d636c7cf8cb39fd0e1aa25c49c339164
parent4d4308c46d4f7801c657cc79d2243e1a81831334 (diff)
downloadmist-41c51b8bdb9c8e9fa4a7d56f260d594739d4107e.tar.gz
mist-41c51b8bdb9c8e9fa4a7d56f260d594739d4107e.tar.bz2
mist-41c51b8bdb9c8e9fa4a7d56f260d594739d4107e.zip
Updated 35 files and added 28 files (automated)
-rw-r--r--.DS_Storebin16388 -> 18436 bytes
-rw-r--r--android/app/release/app-release.apkbin22996167 -> 22996047 bytes
-rw-r--r--android/app/release/app-release.apk.zipbin8300849 -> 8300789 bytes
-rw-r--r--android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt4
-rw-r--r--api/hasStella.php11
-rw-r--r--api/lyrics.php22
-rw-r--r--app/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--app/index.php538
-rw-r--r--app/notes/update-0.3.0.php26
-rw-r--r--app/notes/update-0.4.0.php26
-rw-r--r--app/notes/update-1.0.0.php12
-rw-r--r--app/ui/albums.php20
-rw-r--r--app/ui/download.php5
-rw-r--r--app/ui/explore.php18
-rw-r--r--app/ui/info.php58
-rw-r--r--app/ui/library.php46
-rw-r--r--app/ui/listing.php244
-rw-r--r--app/ui/lyrics.php4
-rw-r--r--app/ui/modal.php29
-rw-r--r--app/ui/navigation.php9
-rw-r--r--app/ui/player-mobile.php60
-rw-r--r--app/ui/player.php99
-rw-r--r--app/ui/queue.php4
-rw-r--r--app/ui/search.php13
-rw-r--r--app/ui/settings.php50
-rw-r--r--app/ui/update.php26
-rw-r--r--app/ui/welcome-dp.php59
-rw-r--r--assets/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--assets/dark.css27
-rw-r--r--assets/icons/back-mobile.svg1
-rw-r--r--assets/icons/back.svg1
-rw-r--r--assets/icons/copy.svg1
-rw-r--r--assets/icons/library.svg1
-rw-r--r--assets/icons/notes-mobile.svg1
-rw-r--r--assets/icons/notes-navigation.svg1
-rw-r--r--assets/icons/notes-normalizer.svg1
-rw-r--r--assets/icons/notes-release.svg1
-rw-r--r--assets/icons/stella.svg1
-rw-r--r--assets/js/common.js4
-rw-r--r--assets/js/normalizer.js40
-rw-r--r--assets/js/pako.js2
-rw-r--r--assets/js/shortcuts.js13
-rw-r--r--assets/js/stella.js32
-rw-r--r--assets/native.css18
-rw-r--r--assets/styles.css75
-rw-r--r--desktop/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--desktop/main.js15
-rw-r--r--includes/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--includes/check.js117
-rw-r--r--includes/process.js21
-rw-r--r--includes/session.php16
-rw-r--r--includes/stella.js97
-rw-r--r--includes/watcher.js2
-rw-r--r--manifest.json4
-rwxr-xr-xnormalizer/normalizer.html171
-rwxr-xr-xnormalizer/sample-a.mp3bin0 -> 37473 bytes
-rwxr-xr-xnormalizer/sample-b.mp3bin0 -> 25377 bytes
-rwxr-xr-xnormalizer/sample-c.mp3bin0 -> 26145 bytes
-rwxr-xr-xnormalizer/sample-d.mp3bin0 -> 36705 bytes
-rwxr-xr-xnormalizer/sample-e.mp3bin0 -> 49761 bytes
-rwxr-xr-xnormalizer/sample-f.mp3bin0 -> 32993 bytes
-rwxr-xr-xnormalizer/sample-g.mp3bin0 -> 53793 bytes
-rw-r--r--version2
63 files changed, 1735 insertions, 313 deletions
diff --git a/.DS_Store b/.DS_Store
index 1afbcc2..cc1b887 100644
--- a/.DS_Store
+++ b/.DS_Store
Binary files differ
diff --git a/android/app/release/app-release.apk b/android/app/release/app-release.apk
index 779688c..d1478d9 100644
--- a/android/app/release/app-release.apk
+++ b/android/app/release/app-release.apk
Binary files differ
diff --git a/android/app/release/app-release.apk.zip b/android/app/release/app-release.apk.zip
index d6ecb7d..089aac4 100644
--- a/android/app/release/app-release.apk.zip
+++ b/android/app/release/app-release.apk.zip
Binary files differ
diff --git a/android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt b/android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt
index 8cb1c90..62c3b03 100644
--- a/android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt
+++ b/android/app/src/main/java/dev/equestria/mist/JavaScriptExtensions.kt
@@ -89,7 +89,7 @@ class JavaScriptExtensions(originalActivity: MainActivity, private val window: W
}
@JavascriptInterface
- fun setNotificationData(title: String, artist: String, album: String, position: Long, duration: Long, playing: Boolean, buffering: Boolean, shuffle: Boolean, repeat: Boolean) {
+ fun setNotificationData(title: String, artist: String, album: String, position: Long, duration: Long, playing: Boolean, buffering: Boolean) {
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = MediaStyle()
@@ -141,7 +141,7 @@ class JavaScriptExtensions(originalActivity: MainActivity, private val window: W
}
override fun onSeekTo(pos: Long) {
- view.post { view.evaluateJavascript("window.document.getElementById(\"player\").contentDocument.getElementById(\"player-audio\").currentTime = $pos / 1000;", null) }
+ view.post { view.evaluateJavascript("window.seekTo($pos / 1000);", null) }
}
}
diff --git a/api/hasStella.php b/api/hasStella.php
new file mode 100644
index 0000000..7e335b7
--- /dev/null
+++ b/api/hasStella.php
@@ -0,0 +1,11 @@
+<?php
+
+header("X-Frame-Options: SAMEORIGIN");
+header("Content-Type: application/json");
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $songs;
+
+if (!isset($_GET["id"]) || !isset($songs[$_GET["id"]])) {
+ die();
+}
+
+die(file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["id"] . ".stella") ? "true" : "false"); \ No newline at end of file
diff --git a/api/lyrics.php b/api/lyrics.php
index 89e7675..11910d9 100644
--- a/api/lyrics.php
+++ b/api/lyrics.php
@@ -3,7 +3,7 @@
header("X-Frame-Options: SAMEORIGIN");
header("Content-Type: application/json");
require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $songs;
-$token = json_decode(file_get_contents("/opt/spotify/token.json"), true);
+$token = json_decode(file_exists("/opt/spotify/token.json") ? file_get_contents("/opt/spotify/token.json") : file_get_contents($_SERVER["DOCUMENT_ROOT"] . "/includes/app.json"), true);
if (!isset($_GET["id"]) || !isset($songs[$_GET["id"]])) {
die();
@@ -19,27 +19,41 @@ $id = json_decode(file_get_contents("https://api.spotify.com/v1/search?q=" . raw
])), true)["tracks"]["items"][0]["id"];
$data = json_decode(file_get_contents("http://localhost:8000/public/?trackid=" . $id), true);
+
if (isset($data) && str_ends_with($data["syncType"], "_SYNCED")) {
die(json_encode([
"synced" => true,
"payload" => $data["lines"]
], JSON_PRETTY_PRINT));
+} elseif (isset($data) && str_starts_with($data["syncType"], "UNSYNCED")) {
+ die(json_encode([
+ "synced" => false,
+ "payload" => implode("\n", array_map(function ($i) {
+ return $i["words"];
+ }, $data["lines"]))
+ ], JSON_PRETTY_PRINT));
}
$genius = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true)["genius"];
-$id = json_decode(file_get_contents("https://api.genius.com/search?q=" . rawurlencode($song["title"] . " " . $song["artist"]), false, stream_context_create([
+$id = array_values(array_filter(json_decode(file_get_contents("https://api.genius.com/search?q=" . rawurlencode($song["title"] . " " . $song["artist"]), false, stream_context_create([
"http" => [
"method" => "GET",
"header" => "Authorization: Bearer " . $genius . "\r\n"
]
-])), true)["response"]["hits"][0]["result"]["id"];
+])), true)["response"]["hits"], function ($i) {
+ return !str_contains(strtolower($i["result"]["artist_names"]), "genius");
+}))[0]["result"]["id"];
$data = [];
exec('bash -c "cd /opt/spotify/spotify-lyrics-api; python genius.py ' . $id . '"', $data);
$data = array_slice(array_map(function ($i) {
if (str_ends_with($i, "1Embed")) {
return substr($i, 0, -6);
+ } elseif (str_ends_with($i, "2Embed")) {
+ return substr($i, 0, -6);
+ } elseif (str_ends_with($i, "Embed")) {
+ return substr($i, 0, -5);
} else {
return $i;
}
@@ -48,7 +62,7 @@ $data = array_slice(array_map(function ($i) {
if (count($data) > 0) {
die(json_encode([
"synced" => false,
- "payload" => implode("\n", $data)
+ "payload" => str_replace("You might also like", "", str_replace("You might also like\n", "", trim(implode("\n", $data))))
], JSON_PRETTY_PRINT));
}
diff --git a/app/.DS_Store b/app/.DS_Store
index 1848aaa..3c6d6b1 100644
--- a/app/.DS_Store
+++ b/app/.DS_Store
Binary files differ
diff --git a/app/index.php b/app/index.php
index 3dc4b44..6989866 100644
--- a/app/index.php
+++ b/app/index.php
@@ -15,6 +15,9 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
<!--<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>-->
<script src="/assets/localforage.min.js"></script>
<script src="/assets/js/shortcuts.js"></script>
+ <script src="/assets/js/normalizer.js"></script>
+ <script src="/assets/js/pako.js"></script>
+ <script src="/assets/js/stella.js"></script>
<link rel="shortcut icon" href="/assets/logo-display.svg" type="image/svg+xml">
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
@@ -30,10 +33,16 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
}
</style>
</head>
-<body <?php if (!str_contains($_SERVER['HTTP_USER_AGENT'], "MistNative/")): ?>class="web"<?php endif; ?>>
+<body <?php if (!str_contains($_SERVER['HTTP_USER_AGENT'], "MistNative/")): ?>class="web"<?php else: ?>class="native"<?php endif; ?>>
<script src="/assets/js/common.js"></script>
<script>
- if (location.hash.trim() === "") location.hash = "#/albums";
+ if (location.hash.trim() === "") {
+ if (window.innerWidth < 863) {
+ location.hash = "#/library";
+ } else {
+ location.hash = "#/albums";
+ }
+ }
if (window.MistNative) {
MistNative.version("<?= explode("|", trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")))[0] ?>", "<?= trim(file_exists("/opt/spotify/build.txt") ? file_get_contents("/opt/spotify/build.txt") : "trunk") ?>");
@@ -41,7 +50,15 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
}
</script>
<div id="loading" style="z-index: 999999; position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background-color: white;">
- <span id="loading-text">Initializing...</span>
+ <img src="/assets/logo-transparent.svg" style="width: 256px; height: 256px;" alt="Mist">
+ <span id="loading-text" style="display: none;">Initializing...</span>
+ </div>
+
+ <div id="mobile-navbar" style="padding: 0 10px; border-bottom: 1px solid rgba(0, 0, 0, .1); height: 48px; top: var(--android-status-bar); left: 0; right: 0; position: fixed; display: none; grid-template-columns: max-content 1fr;">
+ <div style="display: flex; align-items: center;" onclick="document.getElementById('ui').contentWindow.history.back();">
+ <img alt="Back" src="/assets/icons/back-mobile.svg" style="height: 32px; width: 32px;">
+ </div>
+ <div style="display: flex; align-items: center; margin-left: 10px;" id="mobile-navbar-title"></div>
</div>
<iframe title="Player" id="player" src="ui/player.php" style="position: fixed; top: var(--android-status-bar); left: 320px; right: 0; width: calc(100vw - 320px); height: 64px; border-bottom: 1px solid rgba(0, 0, 0, .25); z-index: 9999;"></iframe>
@@ -52,12 +69,42 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
<iframe title="Mobile player" style="background: #ddd; width: 100%; height: 100%;" src="ui/player-mobile.php" id="player-mobile"></iframe>
</div>
+ <div id="mouse-logging" style="display: block; inset: 0; position: fixed; z-index: 99999999; pointer-events: none;"></div>
+ <iframe id="modal" src="ui/modal.php" style="width: 100vw; height: 100vh; border: none; inset: 0; position: fixed; z-index: 99999999; display: none;"></iframe>
+
<script>
+ window.modalLoaded = false;
+ document.getElementById("modal").onload = () => {
+ window.modalLoaded = true;
+
+ <?php $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true); global $_PROFILE; if (in_array($_PROFILE["id"], $app["dp"])): ?>
+ openModal("Mist Developer Preview", "welcome-dp.php", false);
+ <?php else: ?>
+ if (localStorage.getItem("welcomed") !== "true") {
+ openModal("Welcome to Mist", "welcome.php", true);
+ } else {
+ if (localStorage.getItem("lastUpdate") !== "<?= trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")) ?>|<?= trim(file_exists("/opt/spotify/build.txt") ? file_get_contents("/opt/spotify/build.txt") : "trunk") ?>") {
+ openModal("What's new in Mist?", "update.php", true);
+ }
+ }
+ <?php endif; ?>
+ }
+
window.onerror = (_1, _2, _3, _4, err) => {
- document.body.innerHTML = "";
- let pre = document.createElement("pre");
- pre.innerText = err.stack;
- document.body.append(pre);
+ let loadWait = setInterval(() => {
+ if (window.modalLoaded) {
+ clearInterval(loadWait);
+
+ document.getElementById("modal").style.display = "";
+ document.getElementById("modal").contentDocument.getElementById("error-content").innerText = err.stack;
+ document.getElementById("modal").contentWindow._error.show();
+
+ setInterval(() => {
+ document.getElementById("modal").contentWindow._modal.hide();
+ window.parent.document.getElementById("modal").style.display = "";
+ });
+ }
+ });
}
window.playlist = [];
@@ -67,7 +114,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("lyrics-page").style.display = "";
document.getElementById("ui").style.display = "none";
Array.from(document.getElementById("navigation").contentDocument.getElementsByClassName("navigation-item")).map(i => i.classList.remove("active"));
- document.getElementById("navigation").contentDocument.getElementById("lyrics").classList.add("active");
+ if (document.getElementById("navigation").contentDocument.getElementById("lyrics")) document.getElementById("navigation").contentDocument.getElementById("lyrics").classList.add("active");
}
function openUI(name) {
@@ -77,20 +124,45 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("ui").style.display = "";
document.getElementById("ui").src = "ui/" + name + ".php";
Array.from(document.getElementById("navigation").contentDocument.getElementsByClassName("navigation-item")).map(i => i.classList.remove("active"));
- document.getElementById("navigation").contentDocument.getElementById(name).classList.add("active");
+ if (document.getElementById("navigation").contentDocument.getElementById(name)) document.getElementById("navigation").contentDocument.getElementById(name).classList.add("active");
}
- let name = location.hash.split("/")[1];
+ window.onhashchange = window.loadHash = () => {
+ window.name = location.hash.split("/")[1];
- if (name === "lyrics") {
- document.getElementById("ui").src = "ui/albums.php";
- } else if (name === "albums" && location.hash.split("/")[2]) {
- document.getElementById("ui").src = "ui/listing.php?a=" + location.hash.split("/")[2];
- } else {
- document.getElementById("ui").src = "ui/" + name + ".php";
+ function setSrcIfDifferent(src) {
+ if (document.getElementById("ui").contentWindow.location.pathname.substring(5) !== src) {
+ document.getElementById("ui").src = src;
+ }
+ }
+
+ if (name === "lyrics") {
+ showLyrics();
+ } else if (name === "albums" && location.hash.split("/")[2]) {
+ document.getElementById("lyrics-page").style.display = "none";
+ document.getElementById("ui").style.display = "";
+ setSrcIfDifferent("ui/listing.php?a=" + location.hash.split("/")[2]);
+ } else if (name === "search" && location.hash.split("/")[2]) {
+ document.getElementById("lyrics-page").style.display = "none";
+ document.getElementById("ui").style.display = "";
+ setSrcIfDifferent("ui/search.php?q=" + location.hash.split("/")[2]);
+ name = "explore";
+ } else {
+ document.getElementById("lyrics-page").style.display = "none";
+ document.getElementById("ui").style.display = "";
+ setSrcIfDifferent("ui/" + name + ".php");
+ }
}
- document.getElementById("navigation").onload = () => {
+ loadHash();
+
+ document.getElementById("ui").onload = () => {
+ window.resizeHandler();
+ document.getElementById("mobile-navbar-title").innerText = document.getElementById("ui").contentDocument.title;
+ }
+
+ document.getElementById("navigation").onload = window.redoNavigation = (name) => {
+ if (!name || typeof name !== "string") name = window.name;
if (name === "lyrics") showLyrics();
Array.from(document.getElementById("navigation").contentDocument.getElementsByClassName("navigation-item")).map(i => i.classList.remove("active"));
document.getElementById("navigation").contentDocument.getElementById(name).classList.add("active");
@@ -100,35 +172,63 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("player").onload = document.getElementById("player-mobile").onload = () => {
loadedPlayers++;
- if (loadedPlayers === 2) continueLoading();
+ if (loadedPlayers === 2) {
+ window.resizeHandler();
+ continueLoading();
+ }
}
- window.onresize = window.onload = () => {
+ window.onresize = window.onload = window.resizeHandler = () => {
if (window.innerWidth <= 863) {
- document.getElementById("player").contentDocument.getElementById("player").classList.add("mobilified");
+ if (document.getElementById("player").contentDocument && document.getElementById("player").contentDocument.getElementById("player")) document.getElementById("player").contentDocument.getElementById("player").classList.add("mobilified");
+ if (document.getElementById("ui").contentDocument && document.getElementById("ui").contentDocument.body) document.getElementById("ui").contentDocument.body.classList.add("mobile-ui");
} else {
- document.getElementById("player").contentDocument.getElementById("player").classList.remove("mobilified");
+ if (document.getElementById("player").contentDocument && document.getElementById("player").contentDocument.getElementById("player")) document.getElementById("player").contentDocument.getElementById("player").classList.remove("mobilified");
+ if (document.getElementById("ui").contentDocument && document.getElementById("ui").contentDocument.body) document.getElementById("ui").contentDocument.body.classList.remove("mobile-ui");
}
}
+ window.needInitializeNormalizer = false;
+
function continueLoading() {
window.playerDocument = document.getElementById("player").contentDocument;
window.playerDocumentMobile = document.getElementById("player-mobile").contentDocument;
+ window.needInitializeNormalizer = true;
if (!localStorage.getItem("data-saving")) {
localStorage.setItem("data-saving", "false");
}
+ if (!localStorage.getItem("normalize")) {
+ localStorage.setItem("normalize", "true");
+ }
+
if (!localStorage.getItem("desktop-notification")) {
localStorage.setItem("desktop-notification", "true");
}
playerDocument.getElementById("seekbar-container").onclick = (e) => {
playerDocument.getElementById("player-audio").currentTime = (e.offsetX / playerDocument.getElementById("seekbar-container").clientWidth) * playerDocument.getElementById("player-audio").duration;
+
+ if (playingStella) {
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
}
playerDocumentMobile.getElementById("seekbar-container").onclick = (e) => {
playerDocument.getElementById("player-audio").currentTime = (e.offsetX / playerDocumentMobile.getElementById("seekbar-container").clientWidth) * playerDocument.getElementById("player-audio").duration;
+
+ if (playingStella) {
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
}
function parseTime(subject, max) {
@@ -147,6 +247,18 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
next();
}
+ window.seekTo = (time) => {
+ document.getElementById("player").contentDocument.getElementById("player-audio").currentTime = time;
+
+ if (playingStella) {
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ }
+
window.stop = () => {
if (window.MistAndroid) {
window.MistAndroid.removeNotification();
@@ -166,16 +278,10 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("player").contentWindow.location.reload();
document.getElementById("player").onload = () => {
window.playerDocument = document.getElementById("player").contentDocument;
+ window.needInitializeNormalizer = true;
- if (window.innerWidth <= 863) {
- document.getElementById("player").contentDocument.getElementById("player").classList.add("mobilified");
- } else {
- document.getElementById("player").contentDocument.getElementById("player").classList.remove("mobilified");
- }
-
- playerDocument.getElementById("player-audio").ontimeupdate = playerDocument.getElementById("player-audio").onchange = playerDocument.getElementById("player-audio").onunload = playerDocument.getElementById("player-audio").onstop = playerDocument.getElementById("player-audio").onplay = playerDocument.getElementById("player-audio").onpause = () => {
- updateDisplay();
- }
+ window.resizeHandler();
+ initializePlayerDocument();
}
document.getElementById("player-mobile").contentWindow.location.reload();
@@ -192,6 +298,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
if (window.currentSongID !== null) {
document.getElementById("player-mobile-container").style.bottom = "0";
document.getElementById("lyrics-page").classList.add("mobile-show");
+ document.getElementById("lyrics-page").style.pointerEvents = "none";
}
}
@@ -200,6 +307,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("player-mobile-container").style.bottom = "-100vh";
document.getElementById("lyrics-page").classList.remove("mobile-show");
+ document.getElementById("lyrics-page").style.pointerEvents = "";
}
document.getElementById("player-mobile-container").onclick = (e) => {
@@ -210,8 +318,12 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
window.currentPlaylistPosition = 0;
window.buffering = false;
+ window.calledNextRecently = false;
window.next = () => {
+ if (window.calledNextRecently) return;
+ window.calledNextRecently = true;
+
if (window.repeat) {
playlist.push(playlist[currentPlaylistPosition]);
}
@@ -225,6 +337,9 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
}
window.previous = () => {
+ if (window.calledNextRecently) return;
+ window.calledNextRecently = true;
+
if (playlist[currentPlaylistPosition - 1]) {
playSong(playlist[currentPlaylistPosition - 1], "keep");
currentPlaylistPosition--;
@@ -233,22 +348,130 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
}
}
- playerDocument.getElementById("player-audio").ontimeupdate = playerDocument.getElementById("player-audio").onchange = playerDocument.getElementById("player-audio").onunload = playerDocument.getElementById("player-audio").onstop = playerDocument.getElementById("player-audio").onplay = playerDocument.getElementById("player-audio").onpause = () => {
- updateDisplay();
+ function initializePlayerDocument() {
+ playerDocument.getElementById("player-audio").ontimeupdate = playerDocument.getElementById("player-audio").onchange = playerDocument.getElementById("player-audio").onunload = playerDocument.getElementById("player-audio").onstop = () => {
+ updateDisplay();
+
+ if (playingStella) {
+ if (playerDocument.getElementById("player-audio").paused) {
+ if (!playerDocument.getElementById("player-audio-stella-side1").paused) {
+ playerDocument.getElementById("player-audio-stella-side1").pause();
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (!playerDocument.getElementById("player-audio-stella-side2").paused) {
+ playerDocument.getElementById("player-audio-stella-side2").pause();
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (!playerDocument.getElementById("player-audio-stella-side3").paused) {
+ playerDocument.getElementById("player-audio-stella-side3").pause();
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (!playerDocument.getElementById("player-audio-stella-side4").paused) {
+ playerDocument.getElementById("player-audio-stella-side4").pause();
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (!playerDocument.getElementById("player-audio-stella-side5").paused) {
+ playerDocument.getElementById("player-audio-stella-side5").pause();
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ } else {
+ if (playerDocument.getElementById("player-audio-stella-side1").paused) {
+ playerDocument.getElementById("player-audio-stella-side1").play();
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (playerDocument.getElementById("player-audio-stella-side2").paused) {
+ playerDocument.getElementById("player-audio-stella-side2").play();
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (playerDocument.getElementById("player-audio-stella-side3").paused) {
+ playerDocument.getElementById("player-audio-stella-side3").play();
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (playerDocument.getElementById("player-audio-stella-side4").paused) {
+ playerDocument.getElementById("player-audio-stella-side4").play();
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ if (playerDocument.getElementById("player-audio-stella-side5").paused) {
+ playerDocument.getElementById("player-audio-stella-side5").play();
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
+ }
+ }
+ }
+
+ playerDocument.getElementById("player-audio").onplay = () => {
+ if (window.preloadedGains[window.currentSongID]) {
+ window.currentNormalizationSource.connect(window.preloadedGains[window.currentSongID]);
+ window.currentNormalizationSource2.connect(window.preloadedGainsBoosted1[window.currentSongID]);
+ window.currentNormalizationSource3.connect(window.preloadedGainsBoosted2[window.currentSongID]);
+ window.preloadedGains[window.currentSongID].connect(window.currentNormalizationContext.destination);
+ }
+
+ if (playingStella) {
+ for (let player of [
+ window.currentNormalizationSource2,
+ window.currentNormalizationSource3,
+ window.currentNormalizationSource4,
+ window.currentNormalizationSource5
+ ]) {
+ player.connect(window.preloadedGainsBoosted1[window.currentSongID]);
+ window.preloadedGainsBoosted1[window.currentSongID].connect(window.currentNormalizationContext.destination);
+ }
+
+ window.currentNormalizationSource1.connect(window.preloadedGainsBoosted2[window.currentSongID]);
+ window.preloadedGainsBoosted2[window.currentSongID].connect(window.currentNormalizationContext.destination);
+ }
+
+ updateDisplay();
+ }
+
+ playerDocument.getElementById("player-audio").onpause = () => {
+ if (window.preloadedGains[window.currentSongID]) {
+ try {
+ window.currentNormalizationSource.disconnect(window.preloadedGains[window.currentSongID]);
+ window.preloadedGains[window.currentSongID].disconnect(window.currentNormalizationContext.destination);
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (playingStella) {
+ for (let player of [
+ window.currentNormalizationSource2,
+ window.currentNormalizationSource3,
+ window.currentNormalizationSource4,
+ window.currentNormalizationSource5
+ ]) {
+ try {
+ player.disconnect(window.preloadedGainsBoosted1[window.currentSongID]);
+ window.preloadedGainsBoosted1[window.currentSongID].disconnect(window.currentNormalizationContext.destination);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ try {
+ window.currentNormalizationSource1.disconnect(window.preloadedGainsBoosted2[window.currentSongID]);
+ window.preloadedGainsBoosted2[window.currentSongID].disconnect(window.currentNormalizationContext.destination);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ if (playerDocument.getElementById("player-audio").currentTime >= playerDocument.getElementById("player-audio").duration) {
+ next();
+ return;
+ }
+
+ updateDisplay();
+ }
}
+ initializePlayerDocument();
+
function updateDisplay() {
if (window.MistAndroid && currentSong) {
- window.MistAndroid.setNotificationData(currentSong.title,
- currentSong.artist,
- currentSong.album,
- Math.round(playerDocument.getElementById("player-audio").currentTime * 1000),
- Math.round(playerDocument.getElementById("player-audio").duration * 1000),
- !playerDocument.getElementById("player-audio").paused),
- buffering,
- shuffle,
- repeat
- ;
+ window.MistAndroid.setNotificationData(currentSong.title, currentSong.artist, currentSong.album, Math.round(playerDocument.getElementById("player-audio").currentTime * 1000), Math.round(playerDocument.getElementById("player-audio").duration * 1000), !playerDocument.getElementById("player-audio").paused, buffering);
}
if (playerDocument.getElementById("player-audio").paused) {
@@ -297,12 +520,28 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
} else {
playerDocument.getElementById("player-audio").currentTime = 0;
}
+
+ if (playingStella) {
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
});
document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("seekforward", (e) => {
let time = e.seekOffset ?? 10;
if (playerDocument.getElementById("player-audio").currentTime + time < playerDocument.getElementById("player-audio").duration) {
playerDocument.getElementById("player-audio").currentTime += time;
+
+ if (playingStella) {
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
} else {
next();
}
@@ -310,6 +549,14 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("seekto", (e) => {
if (e.seekTime) {
playerDocument.getElementById("player-audio").currentTime = e.seekTime;
+
+ if (playingStella) {
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
}
});
document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("previoustrack", () => {
@@ -317,6 +564,14 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
previous();
} else {
playerDocument.getElementById("player-audio").currentTime = 0;
+
+ if (playingStella) {
+ playerDocument.getElementById("player-audio-stella-side1").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side2").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side3").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side4").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ playerDocument.getElementById("player-audio-stella-side5").currentTime = playerDocument.getElementById("player-audio").currentTime;
+ }
}
});
document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("nexttrack", () => {
@@ -354,33 +609,6 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
playerDocument.getElementById("btn-play-icon").src = playerDocumentMobile.getElementById("btn-play-icon").src = "/assets/icons/pause.svg";
}
- if (localStorage.getItem("data-saving") === "true") {
- playerDocument.getElementById("badge-lossy").style.display = "inline";
- playerDocument.getElementById("badge-cd").style.display = "none";
- playerDocument.getElementById("badge-hires").style.display = "none";
- playerDocumentMobile.getElementById("badge-lossy").style.display = "inline";
- playerDocumentMobile.getElementById("badge-cd").style.display = "none";
- playerDocumentMobile.getElementById("badge-hires").style.display = "none";
- } else {
- if (window.currentSong && window.currentSong.hiRes) {
- playerDocument.getElementById("badge-lossy").style.display = "none";
- playerDocument.getElementById("badge-cd").style.display = "none";
- playerDocument.getElementById("badge-hires").style.display = "inline";
- playerDocument.getElementById("badge-hires").innerHTML = "<span style='display: grid; grid-template-columns: max-content max-content'><span><img src='/assets/icons/lossless.svg' alt='' class='player-badge-icon' style='filter: invert(1);'>Hi-Res Lossless</span><span class='player-badge-desktop'>" + window.currentSong.bitDepth + "-bit " + (window.currentSong.sampleRate / 1000) + " kHz</span>";
- playerDocumentMobile.getElementById("badge-lossy").style.display = "none";
- playerDocumentMobile.getElementById("badge-cd").style.display = "none";
- playerDocumentMobile.getElementById("badge-hires").style.display = "inline";
- } else if (window.currentSong) {
- playerDocument.getElementById("badge-lossy").style.display = "none";
- playerDocument.getElementById("badge-cd").style.display = "inline";
- playerDocument.getElementById("badge-hires").style.display = "none";
- playerDocumentMobile.getElementById("badge-lossy").style.display = "none";
- playerDocumentMobile.getElementById("badge-cd").style.display = "inline";
- playerDocumentMobile.getElementById("badge-hires").style.display = "none";
- playerDocument.getElementById("badge-cd").innerHTML = "<span style='display: grid; grid-template-columns: max-content max-content;'><span><img src='/assets/icons/lossless.svg' alt='' class='player-badge-icon' style='filter: invert(1);'>Lossless</span><span class='player-badge-desktop'>" + window.currentSong.bitDepth + "-bit " + (window.currentSong.sampleRate / 1000) + " kHz</span>";
- }
- }
-
if (window.currentSong) {
playerDocument.getElementById("title").innerText = playerDocumentMobile.getElementById("title").innerText = window.currentSong.title;
playerDocument.getElementById("artist").innerText = playerDocumentMobile.getElementById("artist").innerText = window.currentSong.artist;
@@ -441,9 +669,15 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("loading").style.display = "none";
})();
+ window.playingStella = false;
window.currentSong = null;
window.currentSongID = null;
window.preloaded = {};
+ window.preloadedURLs = {};
+ window.preloadedGains = {};
+ window.preloadedGainsBoosted1 = {};
+ window.preloadedGainsBoosted2 = {};
+ window.preloadedBlobs = {};
window.shuffle = false;
window.repeat = false;
@@ -545,8 +779,33 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
window.currentPlaylistID = null;
window.playSong = async (id, playlistID, updatePosition) => {
+ playerDocument.getElementById("badge-cd").style.display = "none";
+ playerDocument.getElementById("badge-stella").style.display = "none";
+ playerDocument.getElementById("badge-hires").style.display = "none";
+ playerDocumentMobile.getElementById("badge-cd").style.display = "none";
+ playerDocumentMobile.getElementById("badge-stella").style.display = "none";
+ playerDocumentMobile.getElementById("badge-hires").style.display = "none";
+ document.getElementById("player").contentWindow.buildTooltips();
+
+ if (!window.currentNormalizationContext) {
+ window.currentNormalizationContext = new AudioContext();
+ window.currentNormalizationContext2 = new AudioContext();
+ window.currentNormalizationContext3 = new AudioContext();
+ }
+
+ if (window.needInitializeNormalizer) {
+ window.currentNormalizationSource = window.currentNormalizationContext.createMediaElementSource(playerDocument.getElementById("player-audio"));
+ window.currentNormalizationSource1 = window.currentNormalizationContext.createMediaElementSource(playerDocument.getElementById("player-audio-stella-side1"));
+ window.currentNormalizationSource2 = window.currentNormalizationContext.createMediaElementSource(playerDocument.getElementById("player-audio-stella-side2"));
+ window.currentNormalizationSource3 = window.currentNormalizationContext.createMediaElementSource(playerDocument.getElementById("player-audio-stella-side3"));
+ window.currentNormalizationSource4 = window.currentNormalizationContext.createMediaElementSource(playerDocument.getElementById("player-audio-stella-side4"));
+ window.currentNormalizationSource5 = window.currentNormalizationContext.createMediaElementSource(playerDocument.getElementById("player-audio-stella-side5"));
+ window.needInitializeNormalizer = false;
+ }
+
playerDocument.getElementById("player-audio").pause();
playerDocument.getElementById("player-audio").currentTime = 0;
+ window.playingStella = false;
if (playlistID) {
if (playlistID === "favorites") {
@@ -560,7 +819,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
} else if (playlistID !== "keep") {
window.playlist = [id];
window.currentPlaylistPosition = 0;
- } else if (typeof updatePosition !== "boolean" || updatePosition) {
+ } else if (updatePosition !== false) {
window.currentPlaylistPosition = window.playlist.indexOf(id) ?? 0;
}
} else {
@@ -574,22 +833,100 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
updateDisplay();
if (document.getElementById("ui").contentWindow.refreshQueue) document.getElementById("ui").contentWindow.refreshQueue();
- if (!window.preloaded[id]) {
- window.buffering = true;
+ let stellaCompatible = false;
- if (localStorage.getItem("data-saving") === "true") {
- window.preloaded[id] = URL.createObjectURL(new Blob([await (await fetch("/assets/content/" + id + ".m4a")).arrayBuffer()], { type: "audio/mp4" }));
- } else {
- window.preloaded[id] = URL.createObjectURL(new Blob([await (await fetch("/assets/content/" + id + ".flac")).arrayBuffer()], { type: "audio/flac" }));
+ if (localStorage.getItem("enable-stella") === "true" && localStorage.getItem("data-saving") !== "true") {
+ stellaCompatible = await (await fetch("/api/hasStella.php?id=" + id)).text() === "true";
+ }
+
+ if (stellaCompatible) {
+ window.preloaded[id] = await Stella.build("/assets/content/" + id + ".stella");
+ window.preloadedGains[id] = await normalizeAudio(window.preloaded[id].stems.other.buffer, 0);
+ window.preloadedGainsBoosted1[id] = await normalizeAudio(window.preloaded[id].stems.other.buffer, .05);
+ window.preloadedGainsBoosted2[id] = await normalizeAudio(window.preloaded[id].stems.other.buffer, .1);
+ } else {
+ if (!window.preloaded[id]) {
+ window.buffering = true;
+
+ if (localStorage.getItem("data-saving") === "true") {
+ window.preloaded[id] = await (await fetch("/assets/content/" + id + ".m4a")).arrayBuffer();
+ window.preloadedBlobs[id] = new Blob([window.preloaded[id]], { type: "audio/mp4" });
+ } else {
+ window.preloaded[id] = await (await fetch("/assets/content/" + id + ".flac")).arrayBuffer();
+ window.preloadedBlobs[id] = new Blob([window.preloaded[id]], { type: "audio/flac" });
+ }
+
+ window.preloadedGains[id] = await normalizeAudio(window.preloaded[id], 0);
+ window.preloadedGainsBoosted1[id] = await normalizeAudio(window.preloaded[id], .05);
+ window.preloadedGainsBoosted2[id] = await normalizeAudio(window.preloaded[id], .1);
}
}
cleanupPreload();
preloadMore();
- playerDocument.getElementById("player-audio").src = window.preloaded[id];
- playerDocument.getElementById("player-audio").play();
- window.buffering = false;
+ if (!stellaCompatible) {
+ if (!window.preloadedURLs[id]) {
+ window.preloadedURLs[id] = localStorage.getItem("data-saving") ? URL.createObjectURL(window.preloadedBlobs[id]) : URL.createObjectURL(window.preloadedBlobs[id]);
+ }
+ } else {
+ window.playingStella = true;
+ playerDocument.getElementById("player-audio").src = window.preloaded[id].urls.hpf;
+ playerDocument.getElementById("player-audio-stella-side1").src = window.preloaded[id].urls.bass;
+ playerDocument.getElementById("player-audio-stella-side2").src = window.preloaded[id].urls.drums;
+ playerDocument.getElementById("player-audio-stella-side3").src = window.preloaded[id].urls.other;
+ playerDocument.getElementById("player-audio-stella-side4").src = window.preloaded[id].urls.piano;
+ playerDocument.getElementById("player-audio-stella-side5").src = window.preloaded[id].urls.vocals;
+ playerDocument.getElementById("player-audio").play();
+ window.buffering = false;
+ }
+
+ if (!stellaCompatible) {
+ playerDocument.getElementById("player-audio").src = window.preloadedURLs[id];
+ playerDocument.getElementById("player-audio").play();
+ window.buffering = false;
+ }
+
+ window.calledNextRecently = false;
+
+ if (localStorage.getItem("data-saving") === "true") {
+ playerDocument.getElementById("badge-cd").style.display = "none";
+ playerDocument.getElementById("badge-stella").style.display = "none";
+ playerDocument.getElementById("badge-hires").style.display = "none";
+ playerDocumentMobile.getElementById("badge-cd").style.display = "none";
+ playerDocumentMobile.getElementById("badge-stella").style.display = "none";
+ playerDocumentMobile.getElementById("badge-hires").style.display = "none";
+ document.getElementById("player").contentWindow.buildTooltips();
+ } else {
+ if (window.playingStella) {
+ playerDocument.getElementById("badge-cd").style.display = "none";
+ playerDocument.getElementById("badge-stella").style.display = "inline";
+ playerDocument.getElementById("badge-hires").style.display = "none";
+ playerDocumentMobile.getElementById("badge-cd").style.display = "none";
+ playerDocumentMobile.getElementById("badge-stella").style.display = "inline";
+ playerDocumentMobile.getElementById("badge-hires").style.display = "none";
+ document.getElementById("player").contentWindow.buildTooltips();
+ } else {
+ if (window.currentSong && window.currentSong.hiRes) {
+ playerDocument.getElementById("badge-cd").style.display = "none";
+ playerDocument.getElementById("badge-stella").style.display = "none";
+ playerDocument.getElementById("badge-hires").style.display = "inline";
+ playerDocument.getElementById("badge-hires").title = "<b>Hi-Res Lossless</b><br>" + window.currentSong.bitDepth + "-bit " + (window.currentSong.sampleRate / 1000) + " kHz";
+ playerDocumentMobile.getElementById("badge-cd").style.display = "none";
+ playerDocumentMobile.getElementById("badge-stella").style.display = "none";
+ playerDocumentMobile.getElementById("badge-hires").style.display = "inline";
+ document.getElementById("player").contentWindow.buildTooltips();
+ } else if (window.currentSong) {
+ playerDocument.getElementById("badge-cd").style.display = "inline";
+ playerDocument.getElementById("badge-hires").style.display = "none";
+ playerDocumentMobile.getElementById("badge-cd").style.display = "inline";
+ playerDocumentMobile.getElementById("badge-stella").style.display = "none";
+ playerDocumentMobile.getElementById("badge-hires").style.display = "none";
+ playerDocument.getElementById("badge-cd").title = "<b>Lossless</b><br>" + window.currentSong.bitDepth + "-bit " + (window.currentSong.sampleRate / 1000) + " kHz";
+ document.getElementById("player").contentWindow.buildTooltips();
+ }
+ }
+ }
if (window.MistNative && localStorage.getItem("desktop-notification") === "true") {
window.MistNative.notification(currentSong, await (async function() {
@@ -620,6 +957,8 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
if (window.MistAndroid) {
window.MistAndroid.updateNotificationAlbumArt("https://" + location.hostname + "/albumart.php?i=" + currentSongID);
}
+
+ await fetch("/api/addHistory.php?i=" + currentSongID);
}
}
@@ -627,12 +966,30 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
for (let i = 1; i <= 10; i++) {
if (playlist[currentPlaylistPosition + i]) {
let id = playlist[currentPlaylistPosition + i];
+ let stellaCompatible = false;
- if (!window.preloaded[id]) {
- if (localStorage.getItem("data-saving") === "true") {
- window.preloaded[id] = URL.createObjectURL(new Blob([await (await fetch("/assets/content/" + id + ".m4a")).arrayBuffer()], { type: "audio/mp4" }));
- } else {
- window.preloaded[id] = URL.createObjectURL(new Blob([await (await fetch("/assets/content/" + id + ".flac")).arrayBuffer()], { type: "audio/flac" }));
+ if (localStorage.getItem("enable-stella") === "true" && localStorage.getItem("data-saving") !== "true") {
+ stellaCompatible = await (await fetch("/api/hasStella.php?id=" + id)).text() === "true";
+ }
+
+ if (stellaCompatible) {
+ window.preloaded[id] = await Stella.build("/assets/content/" + id + ".stella");
+ window.preloadedGains[id] = await normalizeAudio(window.preloaded[id].stems.other.buffer, 0);
+ window.preloadedGainsBoosted1[id] = await normalizeAudio(window.preloaded[id].stems.other.buffer, .05);
+ window.preloadedGainsBoosted2[id] = await normalizeAudio(window.preloaded[id].stems.other.buffer, .1);
+ } else {
+ if (!window.preloaded[id]) {
+ if (localStorage.getItem("data-saving") === "true") {
+ window.preloaded[id] = await (await fetch("/assets/content/" + id + ".m4a")).arrayBuffer();
+ window.preloadedBlobs[id] = new Blob([window.preloaded[id]], { type: "audio/mp4" });
+ } else {
+ window.preloaded[id] = await (await fetch("/assets/content/" + id + ".flac")).arrayBuffer();
+ window.preloadedBlobs[id] = new Blob([window.preloaded[id]], { type: "audio/flac" });
+ }
+
+ window.preloadedGains[id] = await normalizeAudio(window.preloaded[id], 0);
+ window.preloadedGainsBoosted1[id] = await normalizeAudio(window.preloaded[id], .05);
+ window.preloadedGainsBoosted2[id] = await normalizeAudio(window.preloaded[id], .1);
}
}
}
@@ -646,14 +1003,13 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
for (let key of Object.keys(window.preloaded)) {
if (!keys.includes(key)) {
- URL.revokeObjectURL(window.preloaded[key]);
+ if (window.preloadedURLs[key]) URL.revokeObjectURL(window.preloadedURLs[key]);
delete window.preloaded[key];
}
}
}
</script>
- <iframe id="modal" src="ui/modal.php" style="width: 100vw; height: 100vh; border: none; inset: 0; position: fixed; z-index: 99999; display: none;"></iframe>
<script>
function openModal(title, url, hideTitle) {
document.getElementById("modal").style.display = "";
@@ -670,16 +1026,6 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("modal").contentWindow._modal.show();
}
- document.getElementById("modal").onload = () => {
- if (localStorage.getItem("welcomed") !== "true") {
- openModal("Welcome to Mist", "welcome.php", true);
- } else {
- if (localStorage.getItem("lastUpdate") !== "<?= trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")) ?>|<?= trim(file_exists("/opt/spotify/build.txt") ? file_get_contents("/opt/spotify/build.txt") : "trunk") ?>") {
- openModal("What's new in Mist?", "update.php", true);
- }
- }
- }
-
if (!localStorage.getItem("rich-presence")) {
localStorage.setItem("rich-presence", "true");
}
diff --git a/app/notes/update-0.3.0.php b/app/notes/update-0.3.0.php
new file mode 100644
index 0000000..5661329
--- /dev/null
+++ b/app/notes/update-0.3.0.php
@@ -0,0 +1,26 @@
+<?php if (!isset($releaseNotes)) die(); ?>
+<h2 style="margin-top: 30px;">What's new in Mist?</h2>
+
+<div style="text-align: left; margin-top: 50px;">
+ <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
+ <img src="/assets/icons/notes-security.svg" style="width: 48px;" class="icon" alt="">
+ <div>
+ <div><b>Enhanced security</b></div>
+ <div>Listening to your favorite songs shouldn't come at the expense of your security and your privacy. Mist now includes protections against the most common forms of attacks.</div>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
+ <img src="/assets/icons/notes-connect.svg" style="width: 48px;" class="icon" alt="">
+ <div>
+ <div><b>Mist is now on Discord</b></div>
+ <div>If you are using the desktop app, Mist can now use Discord Rich Presence to show the music you are playing to all of your friends. You will also need the Discord desktop app.</div>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
+ <img src="/assets/icons/notes-android.svg" style="width: 48px;" class="icon" alt="">
+ <div>
+ <div><b>Native Android application</b></div>
+ <div>You can now quit using the Progressive Web App, and instead use the official Mist Android app. Lower battery and resource usage, along with better OS integrations.</div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/notes/update-0.4.0.php b/app/notes/update-0.4.0.php
new file mode 100644
index 0000000..ebdb695
--- /dev/null
+++ b/app/notes/update-0.4.0.php
@@ -0,0 +1,26 @@
+<?php if (!isset($releaseNotes)) die(); ?>
+<h2 style="margin-top: 30px;">What's new in Mist?</h2>
+
+<div style="text-align: left; margin-top: 50px;">
+ <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
+ <img src="/assets/icons/notes-normalizer.svg" style="width: 48px;" class="icon" alt="">
+ <div>
+ <div><b>No more volume switcheroo</b></div>
+ <div>Mist now automagically normalizes audio volume when you are listening to music. You don't need to do any other change and this is enabled by default. Enjoy!</div>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
+ <img src="/assets/icons/notes-mobile.svg" style="width: 48px;" class="icon" alt="">
+ <div>
+ <div><b>Improved mobile experience</b></div>
+ <div>Using Mist on the go shouldn't be frustrating, that's why we refined your mobile experience to make it more pleasant and smoother.</div>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
+ <img src="/assets/icons/notes-navigation.svg" style="width: 48px;" class="icon" alt="">
+ <div>
+ <div><b>Never get lost again</b></div>
+ <div>Navigating Mist was very frustrating and non-pleasant, so we fixed it. You now get improved navigation controls on both desktop and mobile.</div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/notes/update-1.0.0.php b/app/notes/update-1.0.0.php
new file mode 100644
index 0000000..8ced54f
--- /dev/null
+++ b/app/notes/update-1.0.0.php
@@ -0,0 +1,12 @@
+<?php if (!isset($releaseNotes)) die(); ?>
+<h2 style="margin-top: 30px;">What's new in Mist?</h2>
+
+<div style="text-align: left; margin-top: 50px;">
+ <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
+ <img src="/assets/icons/notes-release.svg" style="width: 48px;" class="icon" alt="">
+ <div>
+ <div><b>Stable Mist is there</b></div>
+ <div>As the Developer Preview programme ends, Mist is now stable and can be enjoyed by numerous users. But wait, there's still more coming!</div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/ui/albums.php b/app/ui/albums.php
index e383f81..1ee5822 100644
--- a/app/ui/albums.php
+++ b/app/ui/albums.php
@@ -11,7 +11,7 @@
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>albums</title>
+ <title>Albums</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/dark.css" rel="stylesheet">
<link href="/assets/styles.css" rel="stylesheet">
@@ -21,11 +21,21 @@
<script src="/assets/js/shortcuts.js"></script>
<link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
</head>
-<body class="crossplatform">
+<body class="crossplatform has-navigation">
+ <div id="ui-navigation" style="z-index: 999; background-color: rgba(255, 255, 255, .75); position: fixed; top: 0; left: 0; right: 0; height: 32px; backdrop-filter: blur(50px); -webkit-backdrop-filter: blur(50px);">
+ <div style="display: grid; grid-template-columns: max-content 1fr max-content; height: 100%;" class="container">
+ <div id="ui-back-button" onclick="history.back();" style="display: flex; align-items: center; justify-content: center; text-align: center; opacity: 0; pointer-events: none;">
+ <img src="/assets/icons/back.svg" alt="Back" class="icon">
+ </div>
+ <div style="display: flex; align-items: center; justify-content: center; text-align: center;"><b>Albums</b></div>
+ <div>
+ <input placeholder="Filter" id="filter" class="form-control" style="width: 256px;height: 32px;border-top: none;" onchange="updateFilter();" onkeyup="updateFilter();">
+ </div>
+ </div>
+ </div>
<script src="/assets/js/common.js"></script>
<div class="container">
<br>
- <h2 style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Albums<input placeholder="Filter" id="filter" class="form-control" style="width: 256px; float: right;" onchange="updateFilter();" onkeyup="updateFilter();"></h2>
<div id="album-grid" style="display: grid; grid-template-columns: repeat(5, 1fr);">
<?php global $albums;
@@ -86,11 +96,11 @@
document.getElementById("search-results").style.display = "grid";
document.getElementById("album-grid").style.display = "none";
- let results = fuse.search(query);
+ let results = items.filter(i => i.title.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ").includes(query.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ")) || i.artist.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ").includes(query.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ")));
document.getElementById("search-results").innerHTML = "";
for (let result of results) {
- document.getElementById("search-results").innerHTML += document.getElementById(result.item.id).outerHTML;
+ document.getElementById("search-results").innerHTML += document.getElementById(result.id).outerHTML;
}
} else {
document.getElementById("search-results").style.display = "none";
diff --git a/app/ui/download.php b/app/ui/download.php
index 13f76b0..dad8eef 100644
--- a/app/ui/download.php
+++ b/app/ui/download.php
@@ -58,6 +58,11 @@ function getSize($bytes) {
<ul class="list-group">
<li class="list-group-item">Lossless (FLAC, <?= getSize(filesize($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".flac")) ?>)<a style="float: right;" download="<?= $fileName ?>.flac" href="/assets/content/<?= $_GET["i"] ?>.flac">Download</a></li>
<li class="list-group-item">AAC-LC (MP4, <?= getSize(filesize($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".m4a")) ?>)<a style="float: right;" download="<?= $fileName ?>.m4a" href="/assets/content/<?= $_GET["i"] ?>.m4a">Download</a></li>
+ <?php if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".stella")): ?>
+ <li class="list-group-item">Mist Stella (<?= getSize(filesize($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".stella")) ?>)<a style="float: right;" download="<?= $fileName ?>.stella" href="/assets/content/<?= $_GET["i"] ?>.stella">Download</a></li>
+ <?php else: ?>
+ <li class="list-group-item">Mist Stella (not available)</li>
+ <?php endif; ?>
<li class="list-group-item">Album art (JPEG, <?= getSize(filesize($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".jpg")) ?>)<a style="float: right;" download="<?= $fileName ?>.jpg" href="/assets/content/<?= $_GET["i"] ?>.jpg">Download</a></li>
</ul>
</div>
diff --git a/app/ui/explore.php b/app/ui/explore.php
index 7edfdf1..c93beff 100644
--- a/app/ui/explore.php
+++ b/app/ui/explore.php
@@ -11,7 +11,7 @@
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>explore</title>
+ <title>Explore</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/dark.css" rel="stylesheet">
<link href="/assets/styles.css" rel="stylesheet">
@@ -21,13 +21,23 @@
<script src="/assets/js/shortcuts.js"></script>
<link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
</head>
-<body class="crossplatform">
+<body class="crossplatform has-navigation">
+ <div id="ui-navigation" style="z-index: 999; background-color: rgba(255, 255, 255, .75); position: fixed; top: 0; left: 0; right: 0; height: 32px; backdrop-filter: blur(50px); -webkit-backdrop-filter: blur(50px);">
+ <div style="display: grid; grid-template-columns: max-content 1fr max-content; height: 100%;" class="container">
+ <div id="ui-back-button" onclick="history.back();" style="display: flex; align-items: center; justify-content: center; text-align: center; opacity: 0; pointer-events: none;">
+ <img src="/assets/icons/back.svg" alt="Back" class="icon">
+ </div>
+ <div style="display: flex; align-items: center; justify-content: center; text-align: center;"><b>Explore</b></div>
+ <div style="opacity: 0; pointer-events: none;">
+ <input placeholder="Filter" id="filter" class="form-control" style="width: 256px;height: 32px;border-top: none;" onchange="updateFilter();" onkeyup="updateFilter();">
+ </div>
+ </div>
+ </div>
<script src="/assets/js/common.js"></script>
<div class="container">
<br>
- <h2 style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Explore</h2>
- <form action="search.php">
+ <form action="search.php" onsubmit="window.parent.location.hash = '#/search/' + encodeURIComponent(document.getElementById('search').value);">
<div style="width: calc(100% - 20px); margin-left: 10px;" class="input-group mb-3">
<input name="q" type="text" id="search" class="form-control" placeholder="Search on Mist">
<button class="btn btn-outline-secondary" type="submit" id="btn-search">Search</button>
diff --git a/app/ui/info.php b/app/ui/info.php
index fd4fad5..d21e431 100644
--- a/app/ui/info.php
+++ b/app/ui/info.php
@@ -1,4 +1,4 @@
-<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $songs;
+<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $songs; global $favorites;
if (!isset($_GET["i"]) || !isset($songs[$_GET["i"]])) {
die();
@@ -158,7 +158,22 @@ function getChannelConfiguration($c) {
</tr>
<tr>
<td style="width: calc(100% / 3); text-align: right; padding-right: 10px; opacity: .5;">Mist Stella</td>
- <td>No</td>
+ <td>
+ <?php if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".stella")): ?>
+ <?php
+
+ $handle = fopen($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".stella", "r");
+ fseek($handle, 8);
+ $contents = fread($handle, 512 - 8);
+ fclose($handle);
+ $metadata = json_decode(trim(zlib_decode($contents)), true);
+
+ ?>
+ Yes (<?php if (isset($metadata["id"])): ?>#<?= $metadata["id"] ?>, <?php endif; ?>Version <?= $metadata["version"] ?>)
+ <?php else: ?>
+ No
+ <?php endif; ?>
+ </td>
</tr>
<tr>
<td style="width: calc(100% / 3); text-align: right; padding-right: 10px; opacity: .5;">Copyright</td>
@@ -172,7 +187,20 @@ function getChannelConfiguration($c) {
<td style="width: calc(100% / 3); text-align: right; padding-right: 10px; opacity: .5;">File size (AAC-LC)</td>
<td><?= getSize(filesize($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".m4a")) ?></td>
</tr>
+ <tr>
+ <td style="width: calc(100% / 3); text-align: right; padding-right: 10px; opacity: .5;">File size (Stella)</td>
+ <td>
+ <?php if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".stella")): ?>
+ <?= getSize(filesize($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".stella")) ?>
+ <?php else: ?>
+ -
+ <?php endif; ?>
+ </td>
+ </tr>
</table>
+
+ <hr><?php $id = $_GET["i"]; ?>
+ <a class="btn btn-primary" onclick="<?= in_array($id, $favorites) ? "un" : "" ?>favoriteSong('<?= $id ?>');" id="btn-favorite-<?= $id ?>"><img id="btn-favorite-<?= $id ?>-icon" alt="" src="/assets/icons/favorite-<?= in_array($id, $favorites) ? "on" : "off" ?>.svg" style="pointer-events: none; filter: invert(1); width: 24px; height: 24px; margin-right: 5px;"><span id="btn-favorite-<?= $id ?>-text"><?= in_array($id, $favorites) ? "Remove from favorites" : "Add to favorites" ?></span></a>
</div>
<script>
@@ -182,6 +210,32 @@ function getChannelConfiguration($c) {
window.parent.document.getElementById("modal-frame").style.height = document.body.clientHeight + "px";
}
});
+
+ async function favoriteSong(id) {
+ document.getElementById("btn-favorite-" + id + "-icon").src = "/assets/icons/favorite-on.svg";
+ document.getElementById("btn-favorite-" + id + "-text").innerText = "Remove from favorites";
+ document.getElementById("btn-favorite-" + id).onclick = () => {
+ unfavoriteSong(id);
+ }
+ await fetch("/api/addFavorite.php?i=" + id);
+
+ window.parent.redownloadFavorites();
+ }
+
+ async function unfavoriteSong(id) {
+ document.getElementById("btn-favorite-" + id + "-icon").src = "/assets/icons/favorite-off.svg";
+ document.getElementById("btn-favorite-" + id + "-text").innerText = "Add to favorites";
+ document.getElementById("btn-favorite-" + id).onclick = () => {
+ favoriteSong(id);
+ }
+ await fetch("/api/removeFavorite.php?i=" + id);
+
+ <?php if (isset($favoritesList)): ?>
+ location.reload();
+ <?php endif; ?>
+
+ window.parent.redownloadFavorites();
+ }
</script>
</body>
</html> \ No newline at end of file
diff --git a/app/ui/library.php b/app/ui/library.php
new file mode 100644
index 0000000..c6b5658
--- /dev/null
+++ b/app/ui/library.php
@@ -0,0 +1,46 @@
+<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $songs; global $albums; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <script>
+ if (typeof window.parent.openModal === "undefined") {
+ location.href = "/app/#/library";
+ }
+ </script>
+ <meta charset="UTF-8">
+ <meta name="viewport"
+ content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Library</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/assets/dark.css" rel="stylesheet">
+ <link href="/assets/styles.css" rel="stylesheet">
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
+ <script src="/assets/localforage.min.js"></script>
+ <script src="/assets/fuse.min.js"></script>
+ <script src="/assets/js/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform">
+ <script>
+ window.parent.location.hash = "#/search/<?= rawurlencode($_GET["q"]) ?>";
+ </script>
+ <script src="/assets/js/common.js"></script>
+ <div class="container">
+ <br>
+ <div style="margin-left: 10px;" class="list-group">
+ <a class="list-group-item list-group-item-action" href="albums.php">
+ <img src="/assets/icons/album.svg" style="filter: brightness(0%); margin-right: 5px; vertical-align: middle;">Albums
+ </a>
+ <a class="list-group-item list-group-item-action" href="songs.php">
+ <img src="/assets/icons/song.svg" style="filter: brightness(0%); margin-right: 5px; vertical-align: middle;">Songs
+ </a>
+ <a class="list-group-item list-group-item-action" href="favorites.php">
+ <img src="/assets/icons/favorites.svg" style="filter: brightness(0%); margin-right: 5px; vertical-align: middle;">Favorites
+ </a>
+ </div>
+ </div>
+
+ <br><br>
+</body>
+</html> \ No newline at end of file
diff --git a/app/ui/listing.php b/app/ui/listing.php
index 9ad227f..d32525b 100644
--- a/app/ui/listing.php
+++ b/app/ui/listing.php
@@ -50,7 +50,15 @@ if (!$presetList) {
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>listing</title>
+ <title>
+ <?php if (isset($favoritesList) && !$hasAlbum): ?>
+ Favorites
+ <?php elseif ($hasAlbum): ?>
+ <?= $albums[$_GET["a"]]["title"] ?>
+ <?php else: ?>
+ Songs
+ <?php endif; ?>
+ </title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/dark.css" rel="stylesheet">
<link href="/assets/styles.css" rel="stylesheet">
@@ -60,130 +68,156 @@ if (!$presetList) {
<script src="/assets/fuse.min.js"></script>
<link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
</head>
-<body class="crossplatform">
+<body class="crossplatform has-navigation">
+ <div id="ui-navigation" style="z-index: 999; background-color: rgba(255, 255, 255, .75); position: fixed; top: 0; left: 0; right: 0; height: 32px; backdrop-filter: blur(50px); -webkit-backdrop-filter: blur(50px);">
+ <div style="display: grid; grid-template-columns: max-content 1fr max-content; height: 100%;" class="container">
+ <div id="ui-back-button" onclick="history.back();" style="display: flex; align-items: center; justify-content: center; text-align: center;<?php if (!$hasAlbum): ?>pointer-events: none; opacity: 0;<?php endif; ?>">
+ <img src="/assets/icons/back.svg" alt="Back" class="icon">
+ </div>
+ <div style="display: flex; align-items: center; justify-content: center; text-align: center;"><b><?php if (!$hasAlbum && !isset($favoritesList)): ?>Songs<?php endif; ?></b></div>
+ <?php if (!$hasAlbum): ?>
+ <div>
+ <input placeholder="Filter" id="filter" class="form-control" style="width: 256px;height: 32px;border-top: none;" onchange="updateFilter();" onkeyup="updateFilter();">
+ </div>
+ <?php endif; ?>
+ </div>
+ </div>
<script src="/assets/js/common.js"></script>
<?php if ($hasAlbum): ?>
<script>
window.parent.location.hash = "#/albums/<?= $_GET["a"] ?>";
</script>
<?php endif; ?>
- <div id="content" style="position: fixed; inset: 0; z-index: 10; overflow: auto; backdrop-filter: blur(50vw); -webkit-backdrop-filter: blur(50vw);">
- <div class="container">
- <br>
- <?php if (!$hasAlbum && !isset($favoritesList)): ?>
- <h2 style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Songs<input placeholder="Filter" id="filter" class="form-control" style="width: 256px; float: right;" onchange="updateFilter();" onkeyup="updateFilter();"></h2>
- <?php elseif (isset($favoritesList)): ?>
- <div id="album-info" style="display: grid; grid-template-columns: 20vw 1fr; margin-top: 10px; margin-left: 10px; grid-gap: 30px;">
- <img id="album-info-art" alt="" src="/assets/favorites.svg" style="height: 20vw; width: 20vw; border-radius: .75vw;">
- <div id="album-info-text" style="padding: 30px 0; display: grid; grid-template-rows: 1fr max-content;">
- <div><h2>Favorites</h2>
- <h2 style="opacity: .5;"><?= $_PROFILE["name"] ?></h2>
- <div style="opacity: .5;">
- Click on the heart icon near a song to add it to this list.
- </div>
- </div>
- <div id="album-info-buttons">
- <a class="btn btn-primary <?= count(array_keys($list)) <= 0 ? "disabled" : "" ?>" onclick="window.parent.playSong('<?= array_keys($list)[0] ?? '' ?>', 'favorites');" style="width: 100px;">Play</a>
- <a class="btn btn-outline-primary <?= count(array_keys($list)) <= 0 ? "disabled" : "" ?>" style="width: 100px;" onclick="window.parent.shuffleList('favorites');">Shuffle</a>
- <input placeholder="Filter" id="filter" class="form-control" style="width: 256px; float: right;" onchange="updateFilter();" onkeyup="updateFilter();">
+ <div class="container">
+ <br>
+ <?php if (isset($favoritesList) && !$hasAlbum): ?>
+ <div id="album-info" style="display: grid; grid-template-columns: 20vw 1fr; margin-top: 10px; margin-left: 10px; grid-gap: 30px;">
+ <img id="album-info-art" alt="" src="/assets/favorites.svg" style="height: 20vw; width: 20vw; border-radius: .75vw;">
+ <div id="album-info-text" style="padding: 30px 0; display: grid; grid-template-rows: 1fr max-content;">
+ <div><h2>Favorites</h2>
+ <h2 style="opacity: .5;"><?= $_PROFILE["name"] ?></h2>
+ <div style="opacity: .5;">
+ Click on the heart icon near a song to add it to this list.
</div>
</div>
+ <div id="album-info-buttons">
+ <a class="btn btn-primary <?= count(array_keys($list)) <= 0 ? "disabled" : "" ?>" onclick="window.parent.playSong('<?= array_keys($list)[0] ?? '' ?>', 'favorites');" style="width: 100px;">Play</a>
+ <a class="btn btn-outline-primary <?= count(array_keys($list)) <= 0 ? "disabled" : "" ?>" style="width: 100px;" onclick="window.parent.shuffleList('favorites');">Shuffle</a>
+ </div>
</div>
- <?php else: ?>
- <div id="album-info" style="display: grid; grid-template-columns: 20vw 1fr; margin-top: 10px; margin-left: 10px; grid-gap: 30px;">
- <img id="album-info-art" alt="" src="/assets/content/<?= $_GET["a"] ?>.jpg" style="height: 20vw; width: 20vw; border-radius: .75vw;">
- <div id="album-info-text" style="padding: 30px 0; display: grid; grid-template-rows: 1fr max-content;">
- <div><h2><?= $albums[$_GET["a"]]["title"] ?></h2>
- <h2 style="opacity: .5;"><?= $albums[$_GET["a"]]["artist"] ?></h2>
- <div style="opacity: .5;">
+ </div>
+ <?php elseif ($hasAlbum):
+
+ $albums[$_GET["a"]]["stella"] = false;
+
+ foreach ($albums[$_GET["a"]]["tracks"] as $track) {
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $track . ".stella")) {
+ $albums[$_GET["a"]]["stella"] = true;
+ }
+ }
+
+ ?>
+ <div id="album-info" style="display: grid; grid-template-columns: 20vw 1fr; margin-top: 10px; margin-left: 10px; grid-gap: 30px;">
+ <img id="album-info-art" alt="" src="/assets/content/<?= $_GET["a"] ?>.jpg" style="height: 20vw; width: 20vw; border-radius: .75vw;">
+ <div id="album-info-text" style="padding: 30px 0; display: grid; grid-template-rows: 1fr max-content;">
+ <div><h2><?= $albums[$_GET["a"]]["title"] ?></h2>
+ <h2 style="opacity: .5;"><?= $albums[$_GET["a"]]["artist"] ?></h2>
+ <div style="opacity: .5;">
+ <?php if (isset($albums[$_GET["a"]]["date"]) && $albums[$_GET["a"]]["date"] > 0): ?>
<?= $albums[$_GET["a"]]["date"] ?>
- <?php if ($albums[$_GET["a"]]["hiRes"]): ?>
- · <img src='/assets/icons/lossless.svg' alt='' class='icon player-badge-icon'>Hi-Res Lossless
- <?php endif; ?>
- </div>
- </div>
- <div id="album-info-buttons" <?php if (!in_array($_GET["a"], $library)): ?>class="nolibrary"<?php endif; ?>>
- <?php if (in_array($_GET["a"], $library)): ?>
- <a class="btn btn-primary" onclick="window.parent.playSong('<?= array_keys($list)[0] ?>', 'album:<?= $_GET["a"] ?>');" style="width: 100px;">Play</a>
- <a class="btn btn-outline-primary" style="width: 100px;" onclick="window.parent.shuffleList('album:<?= $_GET["a"] ?>');">Shuffle</a>
- <?php else: ?>
- <a class="btn btn-primary" onclick="window.addToLibrary();" id="library-button" style="width: 200px;">Add to library</a>
- <?php endif ?>
+ <?php if ($albums[$_GET["a"]]["hiRes"] || $albums[$_GET["a"]]["stella"]): ?> · <?php endif; ?>
+ <?php endif; if ($albums[$_GET["a"]]["hiRes"]): ?>
+ <img src='/assets/icons/lossless.svg' alt='' class='icon player-badge-icon'>Hi-Res Lossless
+ <?php if ($albums[$_GET["a"]]["stella"]): ?><span class="mist-stella"> · </span><?php endif; ?>
+ <?php endif; if ($albums[$_GET["a"]]["stella"]): ?>
+ <span class="mist-stella"><img src='/assets/icons/stella.svg' alt='' style="height: 14.4px !important; width: 14.4px !important;" class='icon player-badge-icon'>Mist Stella</span>
+ <?php endif; ?>
</div>
</div>
- </div>
- <?php endif; ?>
- <?php displayList($list, $hasAlbum); ?>
- <div class="list-group" style="margin-left: 10px; margin-top: 20px; display: none;" id="search-results"></div>
- <?php if (count($list) === 0): ?>
- <div class="text-muted" style="position: fixed; display: flex; inset: 0; align-items: center; justify-content: center;">
- <div style="text-align: center;">
- <img class="icon" src="/assets/logo-transparent.svg" style="filter: grayscale(1) invert(1); width: 96px; height: 96px;" alt="">
- <h4 style="opacity: .75;">Add music to your library</h4>
- <p style="max-width: 300px; margin-left: auto; margin-right: auto;">Browse millions of songs and collect your favorites here.</p>
- <div class="btn btn-primary" onclick="window.parent.openUI('explore');">Browse Mist</div>
+ <div id="album-info-buttons" <?php if (!in_array($_GET["a"], $library)): ?>class="nolibrary"<?php endif; ?>>
+ <?php if (in_array($_GET["a"], $library)): ?>
+ <a class="btn btn-primary" onclick="window.parent.playSong('<?= array_keys($list)[0] ?>', 'album:<?= $_GET["a"] ?>');" style="width: 100px;">Play</a>
+ <a class="btn btn-outline-primary" style="width: 100px;" onclick="window.parent.shuffleList('album:<?= $_GET["a"] ?>');">Shuffle</a>
+ <?php else: ?>
+ <a class="btn btn-primary" onclick="window.addToLibrary();" id="library-button" style="width: 200px;">Add to library</a>
+ <?php endif ?>
</div>
</div>
- <?php endif; ?>
- <?php if ($hasAlbum && in_array($_GET["a"], $library)): ?>
- <br>
- <div><a class="link" id="library-button" href="#" onclick="removeFromLibrary();">Remove from library</a></div>
- <?php endif; ?>
+ </div>
+ <?php endif; ?>
+ <?php displayList($list, $hasAlbum); ?>
+ <div class="list-group" style="margin-left: 10px; margin-top: 20px; display: none;" id="search-results"></div>
+ <?php if (count($list) === 0 && !isset($favoritesList)): ?>
+ <div class="text-muted" style="position: fixed; display: flex; inset: 0; align-items: center; justify-content: center;">
+ <div style="text-align: center;">
+ <img class="icon" src="/assets/logo-transparent.svg" style="filter: grayscale(1) invert(1); width: 96px; height: 96px;" alt="">
+ <h4 style="opacity: .75;">Add music to your library</h4>
+ <p style="max-width: 300px; margin-left: auto; margin-right: auto;">Browse millions of songs and collect your favorites here.</p>
+ <div class="btn btn-primary" onclick="window.parent.openUI('explore');">Browse Mist</div>
+ </div>
+ </div>
+ <?php endif; ?>
+ <br>
+ <div style="margin-left: 10px;">
+ <?php if ($hasAlbum && trim($albums[$_GET["a"]]["copyright"]) !== ""): ?>
+ <div class="text-muted"><?= $albums[$_GET["a"]]["copyright"] ?></div>
+ <?php endif; if ($hasAlbum && in_array($_GET["a"], $library)): ?><a class="link" id="library-button" href="#" onclick="removeFromLibrary();">Remove from library</a>
+ <?php endif; ?>
</div>
+ </div>
- <script>
- <?php if ($hasAlbum): ?>
- async function addToLibrary() {
- document.getElementById("library-button").classList.add("disabled");
- await fetch("/api/addLibrary.php?i=<?= $_GET["a"] ?>");
- window.parent.redownloadLibrary();
- location.reload();
- }
+ <script>
+ <?php if ($hasAlbum): ?>
+ async function addToLibrary() {
+ document.getElementById("library-button").classList.add("disabled");
+ await fetch("/api/addLibrary.php?i=<?= $_GET["a"] ?>");
+ window.parent.redownloadLibrary();
+ location.reload();
+ }
- async function removeFromLibrary() {
- document.getElementById("library-button").classList.add("disabled");
- await fetch("/api/removeLibrary.php?i=<?= $_GET["a"] ?>");
- window.parent.redownloadLibrary();
- location.reload();
- }
- <?php endif; ?>
+ async function removeFromLibrary() {
+ document.getElementById("library-button").classList.add("disabled");
+ await fetch("/api/removeLibrary.php?i=<?= $_GET["a"] ?>");
+ window.parent.redownloadLibrary();
+ location.reload();
+ }
+ <?php endif; ?>
+
+ let items = Array.from(document.getElementsByClassName("track")).map(i => { return { title: i.getAttribute("data-item-track"), artist: i.getAttribute("data-item-artist"), id: i.id } });
+
+ const fuse = new Fuse(items, {
+ keys: [
+ {
+ name: 'title',
+ weight: 1
+ },
+ {
+ name: 'artist',
+ weight: .5
+ }
+ ]
+ });
+
+ function updateFilter() {
+ let query = document.getElementById("filter").value.trim();
+
+ if (query !== "") {
+ document.getElementById("search-results").style.display = "flex";
+ document.getElementById("main-list").style.display = "none";
+
+ let results = items.filter(i => i.title.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ").includes(query.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ")) || i.artist.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ").includes(query.toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ")));
+ document.getElementById("search-results").innerHTML = "";
- let items = Array.from(document.getElementsByClassName("track")).map(i => { return { title: i.getAttribute("data-item-track"), artist: i.getAttribute("data-item-artist"), id: i.id } });
-
- const fuse = new Fuse(items, {
- keys: [
- {
- name: 'title',
- weight: 1
- },
- {
- name: 'artist',
- weight: .5
- }
- ]
- });
-
- function updateFilter() {
- let query = document.getElementById("filter").value.trim();
-
- if (query !== "") {
- document.getElementById("search-results").style.display = "flex";
- document.getElementById("main-list").style.display = "none";
-
- let results = fuse.search(query);
- document.getElementById("search-results").innerHTML = "";
-
- for (let result of results) {
- document.getElementById("search-results").innerHTML += document.getElementById(result.item.id).outerHTML;
- }
- } else {
- document.getElementById("search-results").style.display = "none";
- document.getElementById("main-list").style.display = "flex";
+ for (let result of results) {
+ document.getElementById("search-results").innerHTML += document.getElementById(result.id).outerHTML;
}
+ } else {
+ document.getElementById("search-results").style.display = "none";
+ document.getElementById("main-list").style.display = "flex";
}
- </script>
+ }
+ </script>
- <br><br>
- </div>
+ <br><br>
</body>
</html> \ No newline at end of file
diff --git a/app/ui/lyrics.php b/app/ui/lyrics.php
index bd24832..8f50931 100644
--- a/app/ui/lyrics.php
+++ b/app/ui/lyrics.php
@@ -43,7 +43,7 @@
}
</style>
</head>
-<body class="crossplatform" style="background-color: transparent !important;">
+<body class="crossplatform" style="background-color: transparent;">
<script src="/assets/js/common.js"></script>
<div id="lyrics-outer">
<div id="not-playing" style="position: fixed; inset: 16px; display: flex; align-items: center; justify-content: center; opacity: .5; text-align: center;">
@@ -112,6 +112,7 @@
document.getElementById("loading").style.display = "none";
document.getElementById("not-playing").style.display = "none";
+ document.getElementById("lyrics-synced").style.top = "0px";
document.getElementById("lyrics-synced").innerHTML = "<div style='height: 16px;'></div>" + window.lyrics[lastID].payload.map(i => `
<div class="synced-lyrics-item" id="synced-lyrics-${i.startTimeMs}">${i.words}</div>
`).join("") + "<div style='height: 16px;'></div>";
@@ -123,6 +124,7 @@
document.getElementById("loading").style.display = "none";
document.getElementById("not-playing").style.display = "none";
+ document.getElementById("lyrics-unsynced").scrollTop = false;
document.getElementById("lyrics-unsynced").innerText = window.lyrics[lastID].payload.replaceAll("\n\n\n", "\n");
}
} else {
diff --git a/app/ui/modal.php b/app/ui/modal.php
index b8f1cfb..ec3e663 100644
--- a/app/ui/modal.php
+++ b/app/ui/modal.php
@@ -45,5 +45,34 @@
window.parent.document.getElementById("modal").style.display = "none";
});
</script>
+
+ <div class="modal fade" id="error" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header" id="modal-header">
+ <h4 class="modal-title">An internal error has occurred</h4>
+ </div>
+
+ <div class="modal-body">
+ <div class="alert alert-danger">
+ <p>If this is the first time you see this, simply restart Mist and try again.</p>
+ <p>If you have been seeing this multiple times, you might be encountering a bug and you should report it to your administrator.</p>
+ <a onclick="window.parent.location.reload();" class="btn btn-danger">Reload</a>
+
+ <hr>
+ <pre id="error-content"></pre>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ window._error = new bootstrap.Modal(document.getElementById("error"));
+
+ document.getElementById("error").addEventListener("shown.bs.modal", () => {
+ window.parent.document.getElementById("modal").style.display = "";
+ });
+ </script>
</body>
</html> \ No newline at end of file
diff --git a/app/ui/navigation.php b/app/ui/navigation.php
index 9344791..dacaa94 100644
--- a/app/ui/navigation.php
+++ b/app/ui/navigation.php
@@ -39,13 +39,16 @@
<div id="explore" class="navigation-item" onclick="window.parent.openUI('explore');">
<img class="icon" alt="" src="/assets/icons/explore.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Explore</span>
</div>
- <div id="albums" class="navigation-item active" onclick="window.parent.openUI('albums');">
+ <div id="library" class="navigation-item-mobile navigation-item" onclick="window.parent.openUI('library');">
+ <img class="icon" alt="" src="/assets/icons/library.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Library</span>
+ </div>
+ <div id="albums" class="navigation-item-desktop navigation-item active" onclick="window.parent.openUI('albums');">
<img class="icon" alt="" src="/assets/icons/album.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Albums</span>
</div>
- <div id="songs" class="navigation-item" onclick="window.parent.openUI('songs');">
+ <div id="songs" class="navigation-item-desktop navigation-item" onclick="window.parent.openUI('songs');">
<img class="icon" alt="" src="/assets/icons/song.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Songs</span>
</div>
- <div id="favorites" class="navigation-item" onclick="window.parent.openUI('favorites');">
+ <div id="favorites" class="navigation-item-desktop navigation-item" onclick="window.parent.openUI('favorites');">
<img class="icon" alt="" src="/assets/icons/favorites.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Favorites</span>
</div>
<div id="lyrics" class="navigation-item" onclick="window.parent.showLyrics();">
diff --git a/app/ui/player-mobile.php b/app/ui/player-mobile.php
index 5266961..e49e852 100644
--- a/app/ui/player-mobile.php
+++ b/app/ui/player-mobile.php
@@ -44,12 +44,51 @@
document.head.append(style);
</script>
- <div id="act-1" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; top: 0; left: 0; right: 0; height: calc(20px + var(--android-status-bar));"></div>
- <div id="act-2" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; bottom: 0; left: 0; right: 0; height: calc(20px + var(--android-navigation-bar))"></div>
- <div id="act-3" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; bottom: 0; left: 0; top: 0; width: 20px;"></div>
- <div id="act-4" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; bottom: 0; right: 0; top: 0; width: 20px;"></div>
+ <div id="act" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; top: 0; bottom: calc(175px + var(--android-navigation-bar)); height: calc(100vh - (175px + var(--android-navigation-bar))); left: 0; right: 0;"></div>
<div id="android" style="position: fixed; z-index: 9999999; bottom: 0; right: 0; left: 0; height: var(--android-navigation-bar); border-top: 1px solid rgba(255, 255, 255, .1);"></div>
+ <script>
+ window.transitionTimeout = null;
+ window.lastPosY = null;
+ window.initialPosY = null;
+ window.log = false;
+
+ document.body.ontouchmove = (e) => {
+ if (window.log) {
+ window.lastPosY = e.touches[0].clientY;
+ let diff = initialPosY - lastPosY;
+
+ if (diff < 0) {
+ window.parent.document.getElementById("player-mobile-container").style.bottom = diff + "px";
+ window.parent.document.getElementById("lyrics-page").classList.remove("mobile-show");
+ }
+ }
+ }
+
+ document.getElementById("act").ontouchstart = (e) => {
+ window.initialPosY = e.touches[0].clientY;
+
+ if (window.parent.currentSongID !== null) {
+ window.log = true;
+ window.parent.document.getElementById("player-mobile-container").style.transition = "";
+ }
+ }
+
+ document.body.ontouchend = () => {
+ if (window.log) {
+ window.log = false;
+ window.parent.document.getElementById("player-mobile-container").style.transition = "bottom 200ms ease 0s";
+ window.parent.document.getElementById("player-mobile-container").style.bottom = "0";
+ window.parent.document.getElementById("lyrics-page").classList.add("mobile-show");
+ let diff = initialPosY - lastPosY;
+
+ if (diff < 0 && diff <= -10) {
+ window.parent.hideMobilePlayer();
+ }
+ }
+ }
+ </script>
+
<div id="album-art-bg" style="position: fixed;inset: 0;background-position: center;background-size: cover; z-index: 5;"></div>
<div id="album-art-bg2" style="background-color: rgba(0, 0, 0, .1); z-index: 10; position: fixed;inset: 0; backdrop-filter: blur(100px);"></div>
<div id="player" class="bg-white mobile-player" style="background-color: transparent !important; color: white;position: fixed; bottom: var(--android-navigation-bar); left: 0; right: 0; height: 64px; z-index: 9999;">
@@ -87,14 +126,19 @@
</div>
<div style="text-align: right; display: flex; align-items: center; justify-content: right;" id="badges">
<span id="badge-lossy" style="display: none;"></span>
- <span id="badge-cd" style="display: block;border: 1px solid transparent;color: rgba(255, 255, 255, .75);background-color: rgba(255, 255, 255, .25);padding: 2px 5px;border-radius: 5px;font-size: 12px;position: fixed;margin-left: auto;margin-right: auto;width: max-content;left: 0;right: 0;bottom: calc(120px + var(--android-navigation-bar));">
+ <span id="badge-cd" style="display: block;border: 1px solid transparent;color: rgba(255, 255, 255, .5);background-color: rgba(255, 255, 255, .1);padding: 2px 5px;border-radius: 5px;font-size: 12px;position: fixed;margin-left: auto;margin-right: auto;width: max-content;left: 0;right: 0;bottom: calc(120px + var(--android-navigation-bar));">
+ <span style="display: grid; grid-template-columns: max-content max-content">
+ <span><img src="/assets/icons/lossless.svg" alt="" class="player-badge-icon" style="filter: invert(1); opacity: .5;">Lossless</span>
+ </span>
+ </span>
+ <span id="badge-hires" style="display: block;border: 1px solid transparent;color: rgba(255, 255, 255, .5);background-color: rgba(255, 255, 255, .1);padding: 2px 5px;border-radius: 5px;font-size: 12px;position: fixed;margin-left: auto;margin-right: auto;width: max-content;left: 0;right: 0;bottom: calc(120px + var(--android-navigation-bar));">
<span style="display: grid; grid-template-columns: max-content max-content">
- <span><img src="/assets/icons/lossless.svg" alt="" class="player-badge-icon" style="filter: invert(1); opacity: .75;">Lossless</span>
+ <span><img src="/assets/icons/lossless.svg" alt="" class="player-badge-icon" style="filter: invert(1); opacity: .5;">Hi-Res Lossless</span>
</span>
</span>
- <span id="badge-hires" style="display: block;border: 1px solid transparent;color: rgba(255, 255, 255, .75);background-color: rgba(255, 255, 255, .25);padding: 2px 5px;border-radius: 5px;font-size: 12px;position: fixed;margin-left: auto;margin-right: auto;width: max-content;left: 0;right: 0;bottom: calc(120px + var(--android-navigation-bar));">
+ <span id="badge-stella" style="display: block;border: 1px solid transparent;color: rgba(255, 255, 255, .5);background-color: rgba(255, 255, 255, .1);padding: 2px 5px;border-radius: 5px;font-size: 12px;position: fixed;margin-left: auto;margin-right: auto;width: max-content;left: 0;right: 0;bottom: calc(120px + var(--android-navigation-bar));">
<span style="display: grid; grid-template-columns: max-content max-content">
- <span><img src="/assets/icons/lossless.svg" alt="" class="player-badge-icon" style="filter: invert(1); opacity: .75;">Hi-Res Lossless</span>
+ <span><img src="/assets/icons/stella.svg" alt="" class="player-badge-icon" style="height: 14.4px; width: 14.4px !important; filter: invert(1); opacity: .5;">Mist Stella</span>
</span>
</span>
</div>
diff --git a/app/ui/player.php b/app/ui/player.php
index ea60889..8043a5d 100644
--- a/app/ui/player.php
+++ b/app/ui/player.php
@@ -20,10 +20,60 @@
<link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
</head>
<body class="crossplatform">
+ <script>
+ window.transitionTimeout = null;
+ window.lastPosY = null;
+ window.log = false;
+
+ document.body.ontouchmove = (e) => {
+ if (window.log) {
+ console.log(e.touches[0]);
+ window.lastPosY = e.touches[0].clientY +
+ window.parent.document.getElementById("player").offsetTop +
+ (window.parent.innerHeight - window.parent.document.getElementById("player").offsetTop);
+ window.parent.document.getElementById("player-mobile-container").style.bottom = -(e.touches[0].clientY +
+ window.parent.document.getElementById("player").offsetTop +
+ (window.parent.innerHeight - window.parent.document.getElementById("player").offsetTop)) + "px";
+ console.log(window.parent.document.getElementById("player-mobile-container").style.bottom);
+ }
+ }
+
+ document.body.ontouchstart = () => {
+ if (window.parent.currentSongID !== null) {
+ window.log = true;
+ window.parent.document.getElementById("player-mobile-container").style.transition = "";
+ }
+ }
+
+ document.body.ontouchend = () => {
+ if (window.log) {
+ window.log = false;
+ window.parent.document.getElementById("player-mobile-container").style.transition = "bottom 200ms ease 0s";
+ window.parent.document.getElementById("player-mobile-container").style.bottom = "-100vh";
+
+ if (lastPosY) {
+ console.log(window.parent.innerHeight - lastPosY);
+
+ if (window.parent.innerHeight - lastPosY >= 10 && lastPosY >= 10) {
+ window.parent.showMobilePlayer();
+ }
+ }
+
+ window.lastPosY = null;
+ }
+ }
+ </script>
<script src="/assets/js/common.js"></script>
<div id="player" class="bg-white desktop-player" style="position: fixed; bottom: 0; left: 0; right: 0; height: 64px; z-index: 9999;">
<div id="desktop-player-action" onclick="window.parent.showMobilePlayer();" style="display: none;"></div>
+
<audio id="player-audio"></audio>
+ <audio id="player-audio-stella-side1"></audio>
+ <audio id="player-audio-stella-side2"></audio>
+ <audio id="player-audio-stella-side3"></audio>
+ <audio id="player-audio-stella-side4"></audio>
+ <audio id="player-audio-stella-side5"></audio>
+
<div class="container" style="display: grid; grid-template-columns: 1fr 1.5fr 1fr;">
<div id="buttons" style="height: 48px; margin-top: 8px; margin-bottom: 8px;">
<span onclick="window.parent.toggleShuffle();" class="player-btn" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-shuffle">
@@ -43,12 +93,15 @@
</span>
</div>
<div>
+ <span data-bs-html="true" data-bs-toggle="tooltip" id="badge-cd" style="z-index: 9999; display: none;position: absolute;margin-left: 71px;"><img src="/assets/icons/lossless.svg" alt="" style="height: 12px;opacity: .5;" class="icon"></span>
+ <span data-bs-html="true" data-bs-toggle="tooltip" id="badge-hires" style="z-index: 9999; display: none;position: absolute;margin-left: 71px;"><img src="/assets/icons/lossless.svg" alt="" style="height: 12px;opacity: .5;" class="icon"></span>
+ <span data-bs-html="true" title="<b>Mist Stella</b>" data-bs-toggle="tooltip" id="badge-stella" style="z-index: 9999; display: none;position: absolute;margin-left: 71px;"><img src="/assets/icons/stella.svg" alt="" style="height: 12px;opacity: .5;" class="icon"></span>
<div id="info" style="display: none; grid-template-columns: 64px 1fr; height: 64px; border-left: 1px solid rgba(0, 0, 0, .25); border-right: 1px solid rgba(0, 0, 0, .25);">
<img alt="" id="album-art" style="background-color: rgba(0, 0, 0, .1); height: 64px; width: 64px;">
- <div id="info-grid" style="display: grid; grid-template-rows: 2px 22px 22px 12px 6px;">
+ <div id="info-grid" style="z-index: 9; display: grid; grid-template-rows: 2px 22px 22px 12px 6px;">
<div id="info-grid-sep"></div>
- <div id="info-grid-title" style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis; display: flex; font-size: 0.91rem; align-items: end; text-align: center; justify-content: center;"><span id="title">Title</span></div>
- <div id="info-grid-info" style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis; display: flex; font-size: 0.91rem; align-items: start; text-align: center; justify-content: center; opacity: .5;"><span id="artist">Artist</span><span class="player-badge-desktop">&nbsp;—&nbsp;<span id="album">Album</span></span></div>
+ <div id="info-grid-title" style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis; display: flex; font-size: 0.91rem; align-items: end; text-align: center; justify-content: center;"><span onclick="openSong();" class="clickable" id="title">Title</span></div>
+ <div id="info-grid-info" style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis; display: flex; font-size: 0.91rem; align-items: start; text-align: center; justify-content: center; opacity: .5;"><span onclick="openArtist();" class="clickable" id="artist">Artist</span><span class="player-badge-desktop">&nbsp;—&nbsp;<span onclick="openAlbum();" class="clickable" id="album">Album</span></span></div>
<div id="info-grid-time" style="font-size: 9px; opacity: .5; margin-left: 2px; margin-right: 2px;">
<span id="elapsed-time">0:00</span>
<span id="remaining-time" style="float: right;">-0:00</span>
@@ -66,11 +119,45 @@
</div>
</div>
<div style="text-align: right; display: flex; align-items: center; justify-content: right;" id="badges">
- <span id="badge-lossy" style="display: none; border: 1px solid #a402b6; color: white; background-color: #a402b6; padding: 2px 5px; border-radius: 5px; font-size: 12px;"><span style="display: grid; grid-template-columns: max-content max-content"><span>AAC-LC</span><span class="player-badge-desktop">256 kbps</span></span></span>
- <span id="badge-cd" style="display: none; border: 1px solid #02b6a7; color: white; background-color: #02b6a7; padding: 2px 5px; border-radius: 5px; font-size: 12px;"></span>
- <span id="badge-hires" style="display: none; border: 1px solid #b66e02; color: white; background-color: #b66e02; padding: 2px 5px; border-radius: 5px; font-size: 12px;"></span>
+
</div>
</div>
</div>
+
+ <script>
+ window.buildTooltips = () => {
+ console.log("Build tooltip");
+ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
+ [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
+ }
+
+ if (navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("badges").style.marginRight = "96px";
+ }
+
+ function openSong() {
+ window.parent.openModal((window.parent.songs[window.parent.currentSongID]?.artist ?? "Unknown artist") + " - " + (window.parent.songs[window.parent.currentSongID]?.title ?? "Unknown song"), "info.php?i=" + window.parent.currentSongID);
+ }
+
+ function openArtist() {
+ window.parent.location.hash = "#/search/" + encodeURIComponent(window.parent.songs[window.parent.currentSongID]?.artist ?? "Unknown artist");
+ window.parent.document.getElementById("ui").src = "ui/search.php?q=" + encodeURIComponent(window.parent.songs[window.parent.currentSongID]?.artist ?? "Unknown artist")
+ window.parent.redoNavigation("explore");
+ }
+
+ function openAlbum() {
+ window.parent.location.hash = "#/albums/" + Object.entries(window.parent.albums).filter(i => i[1].tracks.includes(window.parent.currentSongID))[0][0];
+ window.parent.redoNavigation("albums");
+ }
+ </script>
+ <style>
+ .clickable {
+ cursor: pointer;
+ }
+
+ .clickable:hover {
+ text-decoration: underline;
+ }
+ </style>
</body>
</html> \ No newline at end of file
diff --git a/app/ui/queue.php b/app/ui/queue.php
index c4403a4..81a7079 100644
--- a/app/ui/queue.php
+++ b/app/ui/queue.php
@@ -11,7 +11,7 @@
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>queue</title>
+ <title>Queue</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/dark.css" rel="stylesheet">
<link href="/assets/styles.css" rel="stylesheet">
@@ -25,7 +25,7 @@
<script src="/assets/js/common.js"></script>
<div class="container">
<br>
- <h2 style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Queue</h2>
+ <h2 class="desktop-title" style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Queue</h2>
<div class="list-group" style="margin-left: 10px; margin-top: 20px;" id="main-list"></div>
<div class="text-muted" style="margin-left: 10px; margin-top: 20px; display: none;" id="empty">
There are no songs playing next. To add songs to the queue, browse your library and select Add to queue.
diff --git a/app/ui/search.php b/app/ui/search.php
index 7d9fbba..0a1c3be 100644
--- a/app/ui/search.php
+++ b/app/ui/search.php
@@ -18,7 +18,7 @@ if (!isset($_GET["q"]) || trim($_GET["q"]) === "" || trim(preg_replace("/ +/m",
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>search</title>
+ <title>Search results for "<?= strip_tags($_GET["q"]) ?>"</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/dark.css" rel="stylesheet">
<link href="/assets/styles.css" rel="stylesheet">
@@ -28,11 +28,18 @@ if (!isset($_GET["q"]) || trim($_GET["q"]) === "" || trim(preg_replace("/ +/m",
<script src="/assets/js/shortcuts.js"></script>
<link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
</head>
-<body class="crossplatform">
+<body class="crossplatform has-navigation">
+ <div id="ui-navigation" style="z-index: 999; background-color: rgba(255, 255, 255, .75); position: fixed; top: 0; left: 0; right: 0; height: 32px; backdrop-filter: blur(50px); -webkit-backdrop-filter: blur(50px);">
+ <div style="display: grid; grid-template-columns: max-content 1fr max-content; height: 100%;" class="container">
+ <div id="ui-back-button" onclick="history.back();" style="display: flex; align-items: center; justify-content: center; text-align: center;">
+ <img src="/assets/icons/back.svg" alt="Back" class="icon">
+ </div>
+ <div style="display: flex; align-items: center; justify-content: center; text-align: center;"><b>Search results for "<?= strip_tags($_GET["q"]) ?>"</b></div>
+ </div>
+ </div>
<script src="/assets/js/common.js"></script>
<div class="container">
<br>
- <h2 style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Search results for "<?= strip_tags($_GET["q"]) ?>"</h2>
<?php
global $albums; global $songs;
diff --git a/app/ui/settings.php b/app/ui/settings.php
index e6dc9ba..34cd095 100644
--- a/app/ui/settings.php
+++ b/app/ui/settings.php
@@ -11,7 +11,7 @@
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>settings</title>
+ <title>Settings</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/dark.css" rel="stylesheet">
<link href="/assets/styles.css" rel="stylesheet">
@@ -25,12 +25,12 @@
<script src="/assets/js/common.js"></script>
<div class="container">
<br>
- <h2 style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Settings</h2>
+ <h2 class="desktop-title" style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Settings</h2>
<div style="margin-left: 10px;">
<div class="form-check form-switch">
<input onchange="saveDS();" class="form-check-input" type="checkbox" role="switch" id="data-saving">
<label class="form-check-label" for="data-saving">Enable data saving</label>
- <div class="text-muted small">Data saving disables playing lossless and high-resolution audio. Instead, you will get 256 kbps AAC-encoded audio, which is high efficient. If you use Bluetooth headphones, the difference should be unnoticeable.</div>
+ <div class="text-muted small">Data saving disables playing lossless and high-resolution audio. Instead, you will get 256 kbps AAC-encoded audio, which is highly efficient. If you use Bluetooth headphones, the difference should be unnoticeable.</div>
</div>
<script>
if (localStorage.getItem("data-saving") === "true") document.getElementById("data-saving").checked = true;
@@ -40,6 +40,35 @@
}
</script>
+ <div class="form-check form-switch" style="margin-top: 10px;">
+ <input onchange="saveN();" class="form-check-input" type="checkbox" role="switch" id="normalize">
+ <label class="form-check-label" for="normalize">Normalize loudness</label>
+ <div class="text-muted small">Normalizing adjusts the volume each song is played at to be the same level for every song. This will avoid you having to change your device's volume between each track, and should typically not be turned off. Powered by ReplayGain.</div>
+ </div>
+ <script>
+ if (localStorage.getItem("normalize") === "true") document.getElementById("normalize").checked = true;
+ function saveN() {
+ localStorage.setItem("normalize", document.getElementById("normalize").checked ? "true" : "false");
+ window.parent.location.reload();
+ }
+ </script>
+
+ <div class="form-check form-switch" id="stella" style="display: none;margin-top: 10px;">
+ <input onchange="saveST();" class="form-check-input" type="checkbox" role="switch" id="enable-stella">
+ <label class="form-check-label" for="enable-stella">Mist Stella</label>
+ <div class="text-muted small">Enjoy your music is a unique way thanks to the Mist Stella spatial audio technology. Stella makes your music feel like it's coming from all around you, giving you a concert-like experience. Note that Stella uses slightly more bandwidth than lossless streaming.</div>
+ </div>
+ <script>
+ if (localStorage.getItem("show-stella-settings") === "true") document.getElementById("stella").style.display = "";
+
+ if (localStorage.getItem("enable-stella") === "true") document.getElementById("enable-stella").checked = true;
+ function saveST() {
+ localStorage.setItem("enable-stella", document.getElementById("enable-stella").checked ? "true" : "false");
+ localStorage.setItem("show-stella-settings", "true");
+ window.parent.location.reload();
+ }
+ </script>
+
<?php if (str_contains($_SERVER['HTTP_USER_AGENT'], "MistNative/")): ?>
<div class="form-check form-switch" style="margin-top: 10px;">
<input onchange="saveDN();" class="form-check-input" type="checkbox" role="switch" id="desktop-notification">
@@ -82,8 +111,21 @@
<?php else: ?>
<div class="text-muted">
<img class="icon" src="/assets/logo-transparent.svg" style="vertical-align: middle; filter: grayscale(1) invert(1); width: 32px; height: 32px;" alt="">
- <span style="vertical-align: middle;">Mist version <?= str_replace("|", " ", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")) ?> (build <?= trim(file_exists("/opt/spotify/build.txt") ? file_get_contents("/opt/spotify/build.txt") : "trunk") ?>) · © <?= date('Y') ?> Equestria.dev</span>
+ <span style="vertical-align: middle;">Mist version <?= str_replace("|", " ", file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")) ?> (build <?= trim(file_exists("/opt/spotify/build.txt") ? file_get_contents("/opt/spotify/build.txt") : "trunk") ?>)<span id="copyright-separator-desktop"> · </span><span id="copyright-separator-mobile"><br></span>© <?= date('Y') ?> Equestria.dev</span>
</div>
+ <style>
+ @media (min-width: 768px) {
+ #copyright-separator-mobile {
+ display: none;
+ }
+ }
+
+ @media (max-width: 767px) {
+ #copyright-separator-desktop {
+ display: none;
+ }
+ }
+ </style>
<?php endif; ?>
</div>
</body>
diff --git a/app/ui/update.php b/app/ui/update.php
index 1117e95..b50dc21 100644
--- a/app/ui/update.php
+++ b/app/ui/update.php
@@ -25,31 +25,7 @@
<script src="/assets/js/common.js"></script>
<div style="padding: 1rem;">
<div style="text-align: center;">
- <h2 style="margin-top: 30px;">What's new in Mist?</h2>
-
- <div style="text-align: left; margin-top: 50px;">
- <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
- <img src="/assets/icons/notes-security.svg" style="width: 48px;" class="icon" alt="">
- <div>
- <div><b>Enhanced security</b></div>
- <div>Listening to your favorite songs shouldn't come at the expense of your security and your privacy. Mist now includes protections against the most common forms of attacks.</div>
- </div>
- </div>
- <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
- <img src="/assets/icons/notes-connect.svg" style="width: 48px;" class="icon" alt="">
- <div>
- <div><b>Mist is now on Discord</b></div>
- <div>If you are using the desktop app, Mist can now use Discord Rich Presence to show the music you are playing to all of your friends. You will also need the Discord desktop app.</div>
- </div>
- </div>
- <div style="display: grid; grid-template-columns: 48px 1fr; grid-gap: 20px; margin-bottom: 20px;">
- <img src="/assets/icons/notes-android.svg" style="width: 48px;" class="icon" alt="">
- <div>
- <div><b>Native Android application</b></div>
- <div>You can now quit using the Progressive Web App, and instead use the official Mist Android app. Lower battery and resource usage, along with better OS integrations.</div>
- </div>
- </div>
- </div>
+ <?php $releaseNotes = true; require_once "../notes/update-1.0.0.php" ?>
<a style="margin-top: 50px; margin-bottom: 30px; display: block;" class="btn btn-primary" onclick="localStorage.setItem('lastUpdate', '<?= trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")) ?>|<?= trim(file_exists("/opt/spotify/build.txt") ? file_get_contents("/opt/spotify/build.txt") : "trunk") ?>'); window.parent._modal.hide();">Continue</a>
</div>
diff --git a/app/ui/welcome-dp.php b/app/ui/welcome-dp.php
new file mode 100644
index 0000000..3986ca3
--- /dev/null
+++ b/app/ui/welcome-dp.php
@@ -0,0 +1,59 @@
+<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <script>
+ if (typeof window.parent.parent.openModal === "undefined") {
+ location.href = "/app/";
+ }
+ </script>
+ <meta charset="UTF-8">
+ <meta name="viewport"
+ content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>welcome-dp</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/assets/dark.css" rel="stylesheet">
+ <link href="/assets/styles.css" rel="stylesheet">
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
+ <script src="/assets/localforage.min.js"></script>
+ <script src="/assets/fuse.min.js"></script>
+ <script src="/assets/js/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform" style="background-color: transparent !important;">
+ <script src="/assets/js/common.js"></script>
+ <div style="padding: 1rem;">
+ <div>
+ <div class="alert alert-warning">
+ <p><b>This is a Developer Preview of Mist meant to be used only for development and experimental purposes.</b></p>
+ <p>If you want to use Mist as your primary streaming platform, you need to register and wait for the stable release instead. Equestria.dev makes no guarantee whatsoever that user data on Developer Preview will remain and reserves the right to revoke access at any time.</p>
+ <p>Thank you for making third-party Mist applications and helping develop the ecosystem. If you need assistance in building your application or hosting it, please <a href="mailto:raindrops@equestria.dev" target="_blank">contact us</a>.</p>
+ <div>While building your application, please keep the following rules in mind:</div>
+ <ul>
+ <li>Sharing content from Mist publicly is illegal.</li>
+ <li>Applications must not use Mist in an abusive way.</li>
+ <li>Applications must not give guest access to Mist.</li>
+ <li>Applications must credit Mist.</li>
+ <li>Applications must be free to use.</li>
+ <li>Do not alter sound quality without notice.</li>
+ <li>Mist Stella implementations should get credit.</li>
+ </ul>
+ <div>Thanks. (version <?= explode("|", trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")))[0] ?>, build <?= trim(file_exists("/opt/spotify/build.txt") ? file_get_contents("/opt/spotify/build.txt") : "trunk") ?>)</div>
+ </div>
+ <div class="btn btn-primary" onclick="window.parent._modal.hide();">Close</div>
+ <hr>
+ <div class="small text-muted">"Mist", "Mist Stella", the Mist logo and the Mist Stella logo are trademarks of Equestria.dev Developers. Mist and Mist Stella are © <?= date('Y') ?> Equestria.dev developers, released under the MIT license.</div>
+ </div>
+ </div>
+
+ <script>
+ window.sizeInterval = setInterval(() => {
+ if (document.body.clientHeight > 0) {
+ clearInterval(sizeInterval);
+ window.parent.document.getElementById("modal-frame").style.height = document.body.clientHeight + "px";
+ }
+ });
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/assets/.DS_Store b/assets/.DS_Store
index f70562d..5ab4458 100644
--- a/assets/.DS_Store
+++ b/assets/.DS_Store
Binary files differ
diff --git a/assets/dark.css b/assets/dark.css
index a5085b1..18e5855 100644
--- a/assets/dark.css
+++ b/assets/dark.css
@@ -1,5 +1,14 @@
@media (prefers-color-scheme: dark) {
- body.crossplatform, #loading {
+ body.web, body.navigation-body, #loading, div#player.bg-white {
+ background-color: #222 !important;
+ }
+
+ body.native #lyrics-page {
+ top: calc(65px + var(--android-status-bar)) !important;
+ background-color: black !important;
+ }
+
+ .crossplatform {
background-color: black !important;
}
@@ -57,7 +66,7 @@
}
.text-muted {
- color: #666 !important;
+ color: #aaa !important;
}
#filter, #search, .btn, .form-check-input, .link, .btn-close {
@@ -115,4 +124,18 @@
border: 1px solid rgba(255, 255, 255, .2);
background-color: #111;
}
+
+ .list-group-item-action:hover {
+ color: inherit;
+ background-color: #151515;
+ }
+
+ .list-group-item-action:active {
+ color: inherit;
+ background-color: #252525;
+ }
+
+ #ui-navigation {
+ background-color: rgba(0, 0, 0, .5) !important;
+ }
} \ No newline at end of file
diff --git a/assets/icons/back-mobile.svg b/assets/icons/back-mobile.svg
new file mode 100644
index 0000000..7e60ad9
--- /dev/null
+++ b/assets/icons/back-mobile.svg
@@ -0,0 +1 @@
+<svg fill="#79c2f6" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m286-452 232 232-39 40-299-299 301-301 39 40-234 234h494v54H286Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/back.svg b/assets/icons/back.svg
new file mode 100644
index 0000000..1c24d08
--- /dev/null
+++ b/assets/icons/back.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m286-452 232 232-39 40-299-299 301-301 39 40-234 234h494v54H286Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
new file mode 100644
index 0000000..f4de1ec
--- /dev/null
+++ b/assets/icons/copy.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M375-262q-36.725 0-61.362-24.638Q289-311.275 289-348v-424q0-36.725 24.638-61.362Q338.275-858 375-858h304q36.725 0 61.362 24.638Q765-808.725 765-772v424q0 36.725-24.638 61.362Q715.725-262 679-262H375Zm0-54h304q12 0 22-10t10-22v-424q0-12-10-22t-22-10H375q-12 0-22 10t-10 22v424q0 12 10 22t22 10ZM241-128q-36.725 0-61.363-24.637Q155-177.275 155-214v-478h54v478q0 12 10 22t22 10h358v54H241Zm102-188v-488 488Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/library.svg b/assets/icons/library.svg
new file mode 100644
index 0000000..f3a51f3
--- /dev/null
+++ b/assets/icons/library.svg
@@ -0,0 +1 @@
+<svg fill="#79c2f6" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M491.085-360Q528-360 553.5-385.56T579-448v-230h116v-70H545v234q-11.116-11-25.058-16.5T491-536q-36.88 0-62.44 25.585-25.56 25.584-25.56 62.5Q403-411 428.585-385.5q25.584 25.5 62.5 25.5ZM335-262q-36.725 0-61.362-24.638Q249-311.275 249-348v-424q0-36.725 24.638-61.362Q298.275-858 335-858h424q36.725 0 61.362 24.638Q845-808.725 845-772v424q0 36.725-24.638 61.362Q795.725-262 759-262H335Zm0-54h424q12 0 22-10t10-22v-424q0-12-10-22t-22-10H335q-12 0-22 10t-10 22v424q0 12 10 22t22 10ZM201-128q-36.725 0-61.363-24.637Q115-177.275 115-214v-478h54v478q0 12 10 22t22 10h478v54H201Zm102-676v488-488Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/notes-mobile.svg b/assets/icons/notes-mobile.svg
new file mode 100644
index 0000000..79b48e9
--- /dev/null
+++ b/assets/icons/notes-mobile.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M308-58q-36.725 0-61.363-24.638Q222-107.275 222-144v-672q0-36.725 24.637-61.362Q271.275-902 308-902h344q36.725 0 61.362 24.638Q738-852.725 738-816v672q0 36.725-24.638 61.362Q688.725-58 652-58H308Zm-32-110v24q0 12 10 22t22 10h344q12 0 22-10t10-22v-24H276Zm0-54h408v-516H276v516Zm0-570h408v-24q0-12-10-22t-22-10H308q-12 0-22 10t-10 22v24Zm0 0v-56 56Zm0 624v56-56Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/notes-navigation.svg b/assets/icons/notes-navigation.svg
new file mode 100644
index 0000000..c8e010d
--- /dev/null
+++ b/assets/icons/notes-navigation.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M524-164 418-424 158-530l-2-36 648-244-244 646h-36Zm16-104 172-450-452 170 200 82 80 198Zm-80-198Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/notes-normalizer.svg b/assets/icons/notes-normalizer.svg
new file mode 100644
index 0000000..8344f7d
--- /dev/null
+++ b/assets/icons/notes-normalizer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M180-180v-241h134v241H180Zm233 0v-600h134v600H413Zm233 0v-401h134v401H646Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/notes-release.svg b/assets/icons/notes-release.svg
new file mode 100644
index 0000000..a3402ac
--- /dev/null
+++ b/assets/icons/notes-release.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m352-86-70-120-136-30 14-140-92-104 92-104-14-140 136-30 70-120 128 54 128-54 70 120 136 30-14 140 92 104-92 104 14 140-136 30-70 120-128-54-128 54Zm24-70 104-44 106 44 58-98 112-26-10-114 74-86-74-86 10-116-112-24-60-98-104 44-106-44-58 98-112 24 10 116-74 86 74 86-10 116 112 24 60 98Zm104-324Zm-42 126 212-212-39-37-173 173-87-89-39 39 126 126Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/stella.svg b/assets/icons/stella.svg
new file mode 100644
index 0000000..abaeea3
--- /dev/null
+++ b/assets/icons/stella.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m315-688 107-143q11.086-14.968 26.329-21.984Q463.571-860 481-860q17.143 0 32.124 7.016Q528.105-845.968 539-831l106.369 142.743L810-632q25 10 37.5 29.661 12.5 19.66 12.5 42.019 0 10.32-3.567 21.391Q852.867-527.857 846-518L739-374l4 157q0 31.145-22.848 54.072Q697.304-140 666-140q-2 0-18-2l-168-51-165.837 50.885Q309-140 304.765-140.5q-4.236-.5-7.765-.5-32.6 0-56.3-21.928Q217-184.855 218-218l4-156-108-145q-6.867-9.957-10.433-20.606Q100-550.254 100-560.902q0-22.929 13.709-43.259Q127.418-624.49 152-633l163-55Zm33 46-163 56q-15 5-20.5 20.5T169-537l110 147-8 159q-1 17 12 27t29 5l168.5-49L649-199q16 5 29-5t12-27l-7-160 109-145q10-13 4.5-28.5T776-585l-164-57-107-143q-9-13-25-13t-25 13L348-642Zm133 144Z"/></svg> \ No newline at end of file
diff --git a/assets/js/common.js b/assets/js/common.js
index 4668316..6ccd25c 100644
--- a/assets/js/common.js
+++ b/assets/js/common.js
@@ -16,7 +16,7 @@ if (window.MistAndroid) {
document.head.append(style);
-if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
- document.getElementById("native-css").disabled = false;
+if (navigator.userAgent.includes("MistNative/darwin")) {
+ if (document.getElementById("native-css")) document.getElementById("native-css").disabled = false;
document.body.classList.remove("crossplatform");
} \ No newline at end of file
diff --git a/assets/js/normalizer.js b/assets/js/normalizer.js
new file mode 100644
index 0000000..c18d242
--- /dev/null
+++ b/assets/js/normalizer.js
@@ -0,0 +1,40 @@
+window.currentNormalizationContext = null;
+window.currentNormalizationContext2 = null;
+window.currentNormalizationContext3 = null;
+
+function normalizeAudio(ab, gainBoost) {
+ ab = ab.slice(0);
+
+ return new Promise((res) => {
+ let currentNormalizationProfile = currentNormalizationContext.createGain();
+ currentNormalizationProfile.gain.value = (1.0 + gainBoost) / 10.0;
+
+ if (localStorage.getItem("normalize") === "false") res(currentNormalizationProfile);
+
+ currentNormalizationContext.decodeAudioData(ab).then(function(decodedData) {
+ let decodedBuffer = decodedData.getChannelData(0);
+ let sliceLen = Math.floor(decodedData.sampleRate * 0.05);
+ let averages = [];
+ let sum = 0.0;
+
+ for (let i = 0; i < decodedBuffer.length; i++) {
+ sum += decodedBuffer[i] ** 2;
+ if (i % sliceLen === 0) {
+ sum = Math.sqrt(sum / sliceLen);
+ averages.push(sum);
+ sum = 0;
+ }
+ }
+
+ averages.sort(function(a, b) { return a - b; });
+ let a = averages[Math.floor(averages.length * 0.95)];
+
+ let gain = (1.0 + gainBoost) / a;
+ gain = gain / 10.0;
+
+ console.log("Calculated gain:", gain);
+ currentNormalizationProfile.gain.value = gain;
+ res(currentNormalizationProfile);
+ });
+ });
+} \ No newline at end of file
diff --git a/assets/js/pako.js b/assets/js/pako.js
new file mode 100644
index 0000000..7b3b985
--- /dev/null
+++ b/assets/js/pako.js
@@ -0,0 +1,2 @@
+/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).pako={})}(this,(function(e){"use strict";var t=(e,t,i,n)=>{let a=65535&e|0,r=e>>>16&65535|0,o=0;for(;0!==i;){o=i>2e3?2e3:i,i-=o;do{a=a+t[n++]|0,r=r+a|0}while(--o);a%=65521,r%=65521}return a|r<<16|0};const i=new Uint32Array((()=>{let e,t=[];for(var i=0;i<256;i++){e=i;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[i]=e}return t})());var n=(e,t,n,a)=>{const r=i,o=a+n;e^=-1;for(let i=a;i<o;i++)e=e>>>8^r[255&(e^t[i])];return-1^e};const a=16209;var r=function(e,t){let i,n,r,o,s,l,d,f,c,h,u,w,b,m,k,_,g,p,v,x,y,E,R,A;const Z=e.state;i=e.next_in,R=e.input,n=i+(e.avail_in-5),r=e.next_out,A=e.output,o=r-(t-e.avail_out),s=r+(e.avail_out-257),l=Z.dmax,d=Z.wsize,f=Z.whave,c=Z.wnext,h=Z.window,u=Z.hold,w=Z.bits,b=Z.lencode,m=Z.distcode,k=(1<<Z.lenbits)-1,_=(1<<Z.distbits)-1;e:do{w<15&&(u+=R[i++]<<w,w+=8,u+=R[i++]<<w,w+=8),g=b[u&k];t:for(;;){if(p=g>>>24,u>>>=p,w-=p,p=g>>>16&255,0===p)A[r++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=b[(65535&g)+(u&(1<<p)-1)];continue t}if(32&p){Z.mode=16191;break e}e.msg="invalid literal/length code",Z.mode=a;break e}v=65535&g,p&=15,p&&(w<p&&(u+=R[i++]<<w,w+=8),v+=u&(1<<p)-1,u>>>=p,w-=p),w<15&&(u+=R[i++]<<w,w+=8,u+=R[i++]<<w,w+=8),g=m[u&_];i:for(;;){if(p=g>>>24,u>>>=p,w-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=m[(65535&g)+(u&(1<<p)-1)];continue i}e.msg="invalid distance code",Z.mode=a;break e}if(x=65535&g,p&=15,w<p&&(u+=R[i++]<<w,w+=8,w<p&&(u+=R[i++]<<w,w+=8)),x+=u&(1<<p)-1,x>l){e.msg="invalid distance too far back",Z.mode=a;break e}if(u>>>=p,w-=p,p=r-o,x>p){if(p=x-p,p>f&&Z.sane){e.msg="invalid distance too far back",Z.mode=a;break e}if(y=0,E=h,0===c){if(y+=d-p,p<v){v-=p;do{A[r++]=h[y++]}while(--p);y=r-x,E=A}}else if(c<p){if(y+=d+c-p,p-=c,p<v){v-=p;do{A[r++]=h[y++]}while(--p);if(y=0,c<v){p=c,v-=p;do{A[r++]=h[y++]}while(--p);y=r-x,E=A}}}else if(y+=c-p,p<v){v-=p;do{A[r++]=h[y++]}while(--p);y=r-x,E=A}for(;v>2;)A[r++]=E[y++],A[r++]=E[y++],A[r++]=E[y++],v-=3;v&&(A[r++]=E[y++],v>1&&(A[r++]=E[y++]))}else{y=r-x;do{A[r++]=A[y++],A[r++]=A[y++],A[r++]=A[y++],v-=3}while(v>2);v&&(A[r++]=A[y++],v>1&&(A[r++]=A[y++]))}break}}break}}while(i<n&&r<s);v=w>>3,i-=v,w-=v<<3,u&=(1<<w)-1,e.next_in=i,e.next_out=r,e.avail_in=i<n?n-i+5:5-(i-n),e.avail_out=r<s?s-r+257:257-(r-s),Z.hold=u,Z.bits=w};const o=15,s=new Uint16Array([3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0]),l=new Uint8Array([16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78]),d=new Uint16Array([1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0]),f=new Uint8Array([16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64]);var c=(e,t,i,n,a,r,c,h)=>{const u=h.bits;let w,b,m,k,_,g,p=0,v=0,x=0,y=0,E=0,R=0,A=0,Z=0,S=0,T=0,O=null;const U=new Uint16Array(16),D=new Uint16Array(16);let I,B,N,C=null;for(p=0;p<=o;p++)U[p]=0;for(v=0;v<n;v++)U[t[i+v]]++;for(E=u,y=o;y>=1&&0===U[y];y--);if(E>y&&(E=y),0===y)return a[r++]=20971520,a[r++]=20971520,h.bits=1,0;for(x=1;x<y&&0===U[x];x++);for(E<x&&(E=x),Z=1,p=1;p<=o;p++)if(Z<<=1,Z-=U[p],Z<0)return-1;if(Z>0&&(0===e||1!==y))return-1;for(D[1]=0,p=1;p<o;p++)D[p+1]=D[p]+U[p];for(v=0;v<n;v++)0!==t[i+v]&&(c[D[t[i+v]]++]=v);if(0===e?(O=C=c,g=20):1===e?(O=s,C=l,g=257):(O=d,C=f,g=0),T=0,v=0,p=x,_=r,R=E,A=0,m=-1,S=1<<E,k=S-1,1===e&&S>852||2===e&&S>592)return 1;for(;;){I=p-A,c[v]+1<g?(B=0,N=c[v]):c[v]>=g?(B=C[c[v]-g],N=O[c[v]-g]):(B=96,N=0),w=1<<p-A,b=1<<R,x=b;do{b-=w,a[_+(T>>A)+b]=I<<24|B<<16|N|0}while(0!==b);for(w=1<<p-1;T&w;)w>>=1;if(0!==w?(T&=w-1,T+=w):T=0,v++,0==--U[p]){if(p===y)break;p=t[i+c[v]]}if(p>E&&(T&k)!==m){for(0===A&&(A=E),_+=x,R=p-A,Z=1<<R;R+A<y&&(Z-=U[R+A],!(Z<=0));)R++,Z<<=1;if(S+=1<<R,1===e&&S>852||2===e&&S>592)return 1;m=T&k,a[m]=E<<24|R<<16|_-r|0}}return 0!==T&&(a[_+T]=p-A<<24|64<<16|0),h.bits=E,0},h={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{Z_FINISH:u,Z_BLOCK:w,Z_TREES:b,Z_OK:m,Z_STREAM_END:k,Z_NEED_DICT:_,Z_STREAM_ERROR:g,Z_DATA_ERROR:p,Z_MEM_ERROR:v,Z_BUF_ERROR:x,Z_DEFLATED:y}=h,E=16180,R=16190,A=16191,Z=16192,S=16194,T=16199,O=16200,U=16206,D=16209,I=e=>(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24);function B(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const N=e=>{if(!e)return 1;const t=e.state;return!t||t.strm!==e||t.mode<E||t.mode>16211?1:0},C=e=>{if(N(e))return g;const t=e.state;return e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=E,t.last=0,t.havedict=0,t.flags=-1,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new Int32Array(852),t.distcode=t.distdyn=new Int32Array(592),t.sane=1,t.back=-1,m},z=e=>{if(N(e))return g;const t=e.state;return t.wsize=0,t.whave=0,t.wnext=0,C(e)},F=(e,t)=>{let i;if(N(e))return g;const n=e.state;return t<0?(i=0,t=-t):(i=5+(t>>4),t<48&&(t&=15)),t&&(t<8||t>15)?g:(null!==n.window&&n.wbits!==t&&(n.window=null),n.wrap=i,n.wbits=t,z(e))},L=(e,t)=>{if(!e)return g;const i=new B;e.state=i,i.strm=e,i.window=null,i.mode=E;const n=F(e,t);return n!==m&&(e.state=null),n};let M,H,j=!0;const K=e=>{if(j){M=new Int32Array(512),H=new Int32Array(32);let t=0;for(;t<144;)e.lens[t++]=8;for(;t<256;)e.lens[t++]=9;for(;t<280;)e.lens[t++]=7;for(;t<288;)e.lens[t++]=8;for(c(1,e.lens,0,288,M,0,e.work,{bits:9}),t=0;t<32;)e.lens[t++]=5;c(2,e.lens,0,32,H,0,e.work,{bits:5}),j=!1}e.lencode=M,e.lenbits=9,e.distcode=H,e.distbits=5},P=(e,t,i,n)=>{let a;const r=e.state;return null===r.window&&(r.wsize=1<<r.wbits,r.wnext=0,r.whave=0,r.window=new Uint8Array(r.wsize)),n>=r.wsize?(r.window.set(t.subarray(i-r.wsize,i),0),r.wnext=0,r.whave=r.wsize):(a=r.wsize-r.wnext,a>n&&(a=n),r.window.set(t.subarray(i-n,i-n+a),r.wnext),(n-=a)?(r.window.set(t.subarray(i-n,i),0),r.wnext=n,r.whave=r.wsize):(r.wnext+=a,r.wnext===r.wsize&&(r.wnext=0),r.whave<r.wsize&&(r.whave+=a))),0};var Y={inflateReset:z,inflateReset2:F,inflateResetKeep:C,inflateInit:e=>L(e,15),inflateInit2:L,inflate:(e,i)=>{let a,o,s,l,d,f,h,B,C,z,F,L,M,H,j,Y,G,X,W,q,J,Q,V=0;const $=new Uint8Array(4);let ee,te;const ie=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(N(e)||!e.output||!e.input&&0!==e.avail_in)return g;a=e.state,a.mode===A&&(a.mode=Z),d=e.next_out,s=e.output,h=e.avail_out,l=e.next_in,o=e.input,f=e.avail_in,B=a.hold,C=a.bits,z=f,F=h,Q=m;e:for(;;)switch(a.mode){case E:if(0===a.wrap){a.mode=Z;break}for(;C<16;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(2&a.wrap&&35615===B){0===a.wbits&&(a.wbits=15),a.check=0,$[0]=255&B,$[1]=B>>>8&255,a.check=n(a.check,$,2,0),B=0,C=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&B)<<8)+(B>>8))%31){e.msg="incorrect header check",a.mode=D;break}if((15&B)!==y){e.msg="unknown compression method",a.mode=D;break}if(B>>>=4,C-=4,J=8+(15&B),0===a.wbits&&(a.wbits=J),J>15||J>a.wbits){e.msg="invalid window size",a.mode=D;break}a.dmax=1<<a.wbits,a.flags=0,e.adler=a.check=1,a.mode=512&B?16189:A,B=0,C=0;break;case 16181:for(;C<16;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(a.flags=B,(255&a.flags)!==y){e.msg="unknown compression method",a.mode=D;break}if(57344&a.flags){e.msg="unknown header flags set",a.mode=D;break}a.head&&(a.head.text=B>>8&1),512&a.flags&&4&a.wrap&&($[0]=255&B,$[1]=B>>>8&255,a.check=n(a.check,$,2,0)),B=0,C=0,a.mode=16182;case 16182:for(;C<32;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}a.head&&(a.head.time=B),512&a.flags&&4&a.wrap&&($[0]=255&B,$[1]=B>>>8&255,$[2]=B>>>16&255,$[3]=B>>>24&255,a.check=n(a.check,$,4,0)),B=0,C=0,a.mode=16183;case 16183:for(;C<16;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}a.head&&(a.head.xflags=255&B,a.head.os=B>>8),512&a.flags&&4&a.wrap&&($[0]=255&B,$[1]=B>>>8&255,a.check=n(a.check,$,2,0)),B=0,C=0,a.mode=16184;case 16184:if(1024&a.flags){for(;C<16;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}a.length=B,a.head&&(a.head.extra_len=B),512&a.flags&&4&a.wrap&&($[0]=255&B,$[1]=B>>>8&255,a.check=n(a.check,$,2,0)),B=0,C=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(L=a.length,L>f&&(L=f),L&&(a.head&&(J=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(o.subarray(l,l+L),J)),512&a.flags&&4&a.wrap&&(a.check=n(a.check,o,L,l)),f-=L,l+=L,a.length-=L),a.length))break e;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===f)break e;L=0;do{J=o[l+L++],a.head&&J&&a.length<65536&&(a.head.name+=String.fromCharCode(J))}while(J&&L<f);if(512&a.flags&&4&a.wrap&&(a.check=n(a.check,o,L,l)),f-=L,l+=L,J)break e}else a.head&&(a.head.name=null);a.length=0,a.mode=16187;case 16187:if(4096&a.flags){if(0===f)break e;L=0;do{J=o[l+L++],a.head&&J&&a.length<65536&&(a.head.comment+=String.fromCharCode(J))}while(J&&L<f);if(512&a.flags&&4&a.wrap&&(a.check=n(a.check,o,L,l)),f-=L,l+=L,J)break e}else a.head&&(a.head.comment=null);a.mode=16188;case 16188:if(512&a.flags){for(;C<16;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(4&a.wrap&&B!==(65535&a.check)){e.msg="header crc mismatch",a.mode=D;break}B=0,C=0}a.head&&(a.head.hcrc=a.flags>>9&1,a.head.done=!0),e.adler=a.check=0,a.mode=A;break;case 16189:for(;C<32;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}e.adler=a.check=I(B),B=0,C=0,a.mode=R;case R:if(0===a.havedict)return e.next_out=d,e.avail_out=h,e.next_in=l,e.avail_in=f,a.hold=B,a.bits=C,_;e.adler=a.check=1,a.mode=A;case A:if(i===w||i===b)break e;case Z:if(a.last){B>>>=7&C,C-=7&C,a.mode=U;break}for(;C<3;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}switch(a.last=1&B,B>>>=1,C-=1,3&B){case 0:a.mode=16193;break;case 1:if(K(a),a.mode=T,i===b){B>>>=2,C-=2;break e}break;case 2:a.mode=16196;break;case 3:e.msg="invalid block type",a.mode=D}B>>>=2,C-=2;break;case 16193:for(B>>>=7&C,C-=7&C;C<32;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if((65535&B)!=(B>>>16^65535)){e.msg="invalid stored block lengths",a.mode=D;break}if(a.length=65535&B,B=0,C=0,a.mode=S,i===b)break e;case S:a.mode=16195;case 16195:if(L=a.length,L){if(L>f&&(L=f),L>h&&(L=h),0===L)break e;s.set(o.subarray(l,l+L),d),f-=L,l+=L,h-=L,d+=L,a.length-=L;break}a.mode=A;break;case 16196:for(;C<14;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(a.nlen=257+(31&B),B>>>=5,C-=5,a.ndist=1+(31&B),B>>>=5,C-=5,a.ncode=4+(15&B),B>>>=4,C-=4,a.nlen>286||a.ndist>30){e.msg="too many length or distance symbols",a.mode=D;break}a.have=0,a.mode=16197;case 16197:for(;a.have<a.ncode;){for(;C<3;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}a.lens[ie[a.have++]]=7&B,B>>>=3,C-=3}for(;a.have<19;)a.lens[ie[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,ee={bits:a.lenbits},Q=c(0,a.lens,0,19,a.lencode,0,a.work,ee),a.lenbits=ee.bits,Q){e.msg="invalid code lengths set",a.mode=D;break}a.have=0,a.mode=16198;case 16198:for(;a.have<a.nlen+a.ndist;){for(;V=a.lencode[B&(1<<a.lenbits)-1],j=V>>>24,Y=V>>>16&255,G=65535&V,!(j<=C);){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(G<16)B>>>=j,C-=j,a.lens[a.have++]=G;else{if(16===G){for(te=j+2;C<te;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(B>>>=j,C-=j,0===a.have){e.msg="invalid bit length repeat",a.mode=D;break}J=a.lens[a.have-1],L=3+(3&B),B>>>=2,C-=2}else if(17===G){for(te=j+3;C<te;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}B>>>=j,C-=j,J=0,L=3+(7&B),B>>>=3,C-=3}else{for(te=j+7;C<te;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}B>>>=j,C-=j,J=0,L=11+(127&B),B>>>=7,C-=7}if(a.have+L>a.nlen+a.ndist){e.msg="invalid bit length repeat",a.mode=D;break}for(;L--;)a.lens[a.have++]=J}}if(a.mode===D)break;if(0===a.lens[256]){e.msg="invalid code -- missing end-of-block",a.mode=D;break}if(a.lenbits=9,ee={bits:a.lenbits},Q=c(1,a.lens,0,a.nlen,a.lencode,0,a.work,ee),a.lenbits=ee.bits,Q){e.msg="invalid literal/lengths set",a.mode=D;break}if(a.distbits=6,a.distcode=a.distdyn,ee={bits:a.distbits},Q=c(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,ee),a.distbits=ee.bits,Q){e.msg="invalid distances set",a.mode=D;break}if(a.mode=T,i===b)break e;case T:a.mode=O;case O:if(f>=6&&h>=258){e.next_out=d,e.avail_out=h,e.next_in=l,e.avail_in=f,a.hold=B,a.bits=C,r(e,F),d=e.next_out,s=e.output,h=e.avail_out,l=e.next_in,o=e.input,f=e.avail_in,B=a.hold,C=a.bits,a.mode===A&&(a.back=-1);break}for(a.back=0;V=a.lencode[B&(1<<a.lenbits)-1],j=V>>>24,Y=V>>>16&255,G=65535&V,!(j<=C);){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(Y&&0==(240&Y)){for(X=j,W=Y,q=G;V=a.lencode[q+((B&(1<<X+W)-1)>>X)],j=V>>>24,Y=V>>>16&255,G=65535&V,!(X+j<=C);){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}B>>>=X,C-=X,a.back+=X}if(B>>>=j,C-=j,a.back+=j,a.length=G,0===Y){a.mode=16205;break}if(32&Y){a.back=-1,a.mode=A;break}if(64&Y){e.msg="invalid literal/length code",a.mode=D;break}a.extra=15&Y,a.mode=16201;case 16201:if(a.extra){for(te=a.extra;C<te;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}a.length+=B&(1<<a.extra)-1,B>>>=a.extra,C-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;V=a.distcode[B&(1<<a.distbits)-1],j=V>>>24,Y=V>>>16&255,G=65535&V,!(j<=C);){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(0==(240&Y)){for(X=j,W=Y,q=G;V=a.distcode[q+((B&(1<<X+W)-1)>>X)],j=V>>>24,Y=V>>>16&255,G=65535&V,!(X+j<=C);){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}B>>>=X,C-=X,a.back+=X}if(B>>>=j,C-=j,a.back+=j,64&Y){e.msg="invalid distance code",a.mode=D;break}a.offset=G,a.extra=15&Y,a.mode=16203;case 16203:if(a.extra){for(te=a.extra;C<te;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}a.offset+=B&(1<<a.extra)-1,B>>>=a.extra,C-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){e.msg="invalid distance too far back",a.mode=D;break}a.mode=16204;case 16204:if(0===h)break e;if(L=F-h,a.offset>L){if(L=a.offset-L,L>a.whave&&a.sane){e.msg="invalid distance too far back",a.mode=D;break}L>a.wnext?(L-=a.wnext,M=a.wsize-L):M=a.wnext-L,L>a.length&&(L=a.length),H=a.window}else H=s,M=d-a.offset,L=a.length;L>h&&(L=h),h-=L,a.length-=L;do{s[d++]=H[M++]}while(--L);0===a.length&&(a.mode=O);break;case 16205:if(0===h)break e;s[d++]=a.length,h--,a.mode=O;break;case U:if(a.wrap){for(;C<32;){if(0===f)break e;f--,B|=o[l++]<<C,C+=8}if(F-=h,e.total_out+=F,a.total+=F,4&a.wrap&&F&&(e.adler=a.check=a.flags?n(a.check,s,F,d-F):t(a.check,s,F,d-F)),F=h,4&a.wrap&&(a.flags?B:I(B))!==a.check){e.msg="incorrect data check",a.mode=D;break}B=0,C=0}a.mode=16207;case 16207:if(a.wrap&&a.flags){for(;C<32;){if(0===f)break e;f--,B+=o[l++]<<C,C+=8}if(4&a.wrap&&B!==(4294967295&a.total)){e.msg="incorrect length check",a.mode=D;break}B=0,C=0}a.mode=16208;case 16208:Q=k;break e;case D:Q=p;break e;case 16210:return v;default:return g}return e.next_out=d,e.avail_out=h,e.next_in=l,e.avail_in=f,a.hold=B,a.bits=C,(a.wsize||F!==e.avail_out&&a.mode<D&&(a.mode<U||i!==u))&&P(e,e.output,e.next_out,F-e.avail_out),z-=e.avail_in,F-=e.avail_out,e.total_in+=z,e.total_out+=F,a.total+=F,4&a.wrap&&F&&(e.adler=a.check=a.flags?n(a.check,s,F,e.next_out-F):t(a.check,s,F,e.next_out-F)),e.data_type=a.bits+(a.last?64:0)+(a.mode===A?128:0)+(a.mode===T||a.mode===S?256:0),(0===z&&0===F||i===u)&&Q===m&&(Q=x),Q},inflateEnd:e=>{if(N(e))return g;let t=e.state;return t.window&&(t.window=null),e.state=null,m},inflateGetHeader:(e,t)=>{if(N(e))return g;const i=e.state;return 0==(2&i.wrap)?g:(i.head=t,t.done=!1,m)},inflateSetDictionary:(e,i)=>{const n=i.length;let a,r,o;return N(e)?g:(a=e.state,0!==a.wrap&&a.mode!==R?g:a.mode===R&&(r=1,r=t(r,i,n,0),r!==a.check)?p:(o=P(e,i,n,n),o?(a.mode=16210,v):(a.havedict=1,m)))},inflateInfo:"pako inflate (from Nodeca project)"};const G=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var X=function(e){const t=Array.prototype.slice.call(arguments,1);for(;t.length;){const i=t.shift();if(i){if("object"!=typeof i)throw new TypeError(i+"must be non-object");for(const t in i)G(i,t)&&(e[t]=i[t])}}return e},W=e=>{let t=0;for(let i=0,n=e.length;i<n;i++)t+=e[i].length;const i=new Uint8Array(t);for(let t=0,n=0,a=e.length;t<a;t++){let a=e[t];i.set(a,n),n+=a.length}return i};let q=!0;try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(e){q=!1}const J=new Uint8Array(256);for(let e=0;e<256;e++)J[e]=e>=252?6:e>=248?5:e>=240?4:e>=224?3:e>=192?2:1;J[254]=J[254]=1;var Q=e=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(e);let t,i,n,a,r,o=e.length,s=0;for(a=0;a<o;a++)i=e.charCodeAt(a),55296==(64512&i)&&a+1<o&&(n=e.charCodeAt(a+1),56320==(64512&n)&&(i=65536+(i-55296<<10)+(n-56320),a++)),s+=i<128?1:i<2048?2:i<65536?3:4;for(t=new Uint8Array(s),r=0,a=0;r<s;a++)i=e.charCodeAt(a),55296==(64512&i)&&a+1<o&&(n=e.charCodeAt(a+1),56320==(64512&n)&&(i=65536+(i-55296<<10)+(n-56320),a++)),i<128?t[r++]=i:i<2048?(t[r++]=192|i>>>6,t[r++]=128|63&i):i<65536?(t[r++]=224|i>>>12,t[r++]=128|i>>>6&63,t[r++]=128|63&i):(t[r++]=240|i>>>18,t[r++]=128|i>>>12&63,t[r++]=128|i>>>6&63,t[r++]=128|63&i);return t},V=(e,t)=>{const i=t||e.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(e.subarray(0,t));let n,a;const r=new Array(2*i);for(a=0,n=0;n<i;){let t=e[n++];if(t<128){r[a++]=t;continue}let o=J[t];if(o>4)r[a++]=65533,n+=o-1;else{for(t&=2===o?31:3===o?15:7;o>1&&n<i;)t=t<<6|63&e[n++],o--;o>1?r[a++]=65533:t<65536?r[a++]=t:(t-=65536,r[a++]=55296|t>>10&1023,r[a++]=56320|1023&t)}}return((e,t)=>{if(t<65534&&e.subarray&&q)return String.fromCharCode.apply(null,e.length===t?e:e.subarray(0,t));let i="";for(let n=0;n<t;n++)i+=String.fromCharCode(e[n]);return i})(r,a)},$=(e,t)=>{(t=t||e.length)>e.length&&(t=e.length);let i=t-1;for(;i>=0&&128==(192&e[i]);)i--;return i<0||0===i?t:i+J[e[i]]>t?i:t},ee={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"};var te=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};var ie=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const ne=Object.prototype.toString,{Z_NO_FLUSH:ae,Z_FINISH:re,Z_OK:oe,Z_STREAM_END:se,Z_NEED_DICT:le,Z_STREAM_ERROR:de,Z_DATA_ERROR:fe,Z_MEM_ERROR:ce}=h;function he(e){this.options=X({chunkSize:65536,windowBits:15,to:""},e||{});const t=this.options;t.raw&&t.windowBits>=0&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(t.windowBits>=0&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),t.windowBits>15&&t.windowBits<48&&0==(15&t.windowBits)&&(t.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new te,this.strm.avail_out=0;let i=Y.inflateInit2(this.strm,t.windowBits);if(i!==oe)throw new Error(ee[i]);if(this.header=new ie,Y.inflateGetHeader(this.strm,this.header),t.dictionary&&("string"==typeof t.dictionary?t.dictionary=Q(t.dictionary):"[object ArrayBuffer]"===ne.call(t.dictionary)&&(t.dictionary=new Uint8Array(t.dictionary)),t.raw&&(i=Y.inflateSetDictionary(this.strm,t.dictionary),i!==oe)))throw new Error(ee[i])}function ue(e,t){const i=new he(t);if(i.push(e),i.err)throw i.msg||ee[i.err];return i.result}he.prototype.push=function(e,t){const i=this.strm,n=this.options.chunkSize,a=this.options.dictionary;let r,o,s;if(this.ended)return!1;for(o=t===~~t?t:!0===t?re:ae,"[object ArrayBuffer]"===ne.call(e)?i.input=new Uint8Array(e):i.input=e,i.next_in=0,i.avail_in=i.input.length;;){for(0===i.avail_out&&(i.output=new Uint8Array(n),i.next_out=0,i.avail_out=n),r=Y.inflate(i,o),r===le&&a&&(r=Y.inflateSetDictionary(i,a),r===oe?r=Y.inflate(i,o):r===fe&&(r=le));i.avail_in>0&&r===se&&i.state.wrap>0&&0!==e[i.next_in];)Y.inflateReset(i),r=Y.inflate(i,o);switch(r){case de:case fe:case le:case ce:return this.onEnd(r),this.ended=!0,!1}if(s=i.avail_out,i.next_out&&(0===i.avail_out||r===se))if("string"===this.options.to){let e=$(i.output,i.next_out),t=i.next_out-e,a=V(i.output,e);i.next_out=t,i.avail_out=n-t,t&&i.output.set(i.output.subarray(e,e+t),0),this.onData(a)}else this.onData(i.output.length===i.next_out?i.output:i.output.subarray(0,i.next_out));if(r!==oe||0!==s){if(r===se)return r=Y.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,!0;if(0===i.avail_in)break}}return!0},he.prototype.onData=function(e){this.chunks.push(e)},he.prototype.onEnd=function(e){e===oe&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=W(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg};var we=he,be=ue,me=function(e,t){return(t=t||{}).raw=!0,ue(e,t)},ke=ue,_e=h,ge={Inflate:we,inflate:be,inflateRaw:me,ungzip:ke,constants:_e};e.Inflate=we,e.constants=_e,e.default=ge,e.inflate=be,e.inflateRaw=me,e.ungzip=ke,Object.defineProperty(e,"__esModule",{value:!0})})); \ No newline at end of file
diff --git a/assets/js/shortcuts.js b/assets/js/shortcuts.js
index 2fa48ca..6ed3c67 100644
--- a/assets/js/shortcuts.js
+++ b/assets/js/shortcuts.js
@@ -1,24 +1,27 @@
document.onkeydown = (e) => {
-
- if ((e.metaKey || e.ctrlKey) && e.key === ".") {
+ if ((e.metaKey || e.ctrlKey) && e.code === "Comma") {
+ if (window.parent.currentSong === null) return;
e.preventDefault();
window.parent.stop();
return false;
}
- if ((e.metaKey || e.ctrlKey) && e.key === "ArrowRight") {
+ if ((e.metaKey || e.ctrlKey) && e.code === "ArrowRight") {
+ if (window.parent.currentSong === null) return;
e.preventDefault();
window.parent.next();
return false;
}
- if ((e.metaKey || e.ctrlKey) && e.key === "ArrowLeft") {
+ if ((e.metaKey || e.ctrlKey) && e.code === "ArrowLeft") {
+ if (window.parent.currentSong === null) return;
e.preventDefault();
window.parent.previous();
return false;
}
- if (e.key === " " && e.target.tagName !== "INPUT") {
+ if (e.code === "Space" && e.target.tagName !== "INPUT") {
+ if (window.parent.currentSong === null) return;
e.preventDefault();
window.parent.playPause();
return false;
diff --git a/assets/js/stella.js b/assets/js/stella.js
new file mode 100644
index 0000000..aa6bbaa
--- /dev/null
+++ b/assets/js/stella.js
@@ -0,0 +1,32 @@
+class Stella {
+ arrayBuffer;
+ metadata;
+ stems;
+
+ constructor(ab) {
+ this.arrayBuffer = ab;
+ this.metadata = JSON.parse(new TextDecoder("utf-8").decode(pako.inflateRaw(ab.slice(8, 512))).trim());
+ this.stems = {};
+ this.urls = {};
+
+ for (let stem of Object.keys(this.metadata.stems)) {
+ this.stems[stem] = pako.inflateRaw(ab.slice(this.metadata.stems[stem][0], this.metadata.stems[stem][0] + this.metadata.stems[stem][1]));
+ this.urls[stem] = URL.createObjectURL(new Blob([this.stems[stem]], { type: "audio/flac" }));
+ }
+ }
+
+ destroy() {
+ for (let url of Object.values(this.urls)) {
+ URL.revokeObjectURL(url);
+ }
+
+ this.urls = null;
+ this.stems = null;
+ this.arrayBuffer = null;
+ }
+
+ static async build(url) {
+ let buffer = await (await fetch(url)).arrayBuffer();
+ return new Stella(buffer);
+ }
+} \ No newline at end of file
diff --git a/assets/native.css b/assets/native.css
index 79260eb..1d892a5 100644
--- a/assets/native.css
+++ b/assets/native.css
@@ -1,7 +1,17 @@
-body, #player.bg-white {
+body.native {
background-color: transparent !important;
}
+#ui, #lyrics-page, #lyrics-body {
+ background-color: white !important;
+}
+
+@media (prefers-color-scheme: dark) {
+ #ui, #lyrics-page, #lyrics-body {
+ background-color: black !important;
+ }
+}
+
body.navigation-body {
background-color: rgba(255, 255, 255, .1) !important;
border-right: 1px solid rgba(0, 0, 0, .1) !important;
@@ -13,7 +23,7 @@ body.navigation-body {
display: none !important;
}
-#filter, #search, .list-group-item {
+#search, .list-group-item {
background-color: rgba(255, 255, 255, .25) !important;
}
@@ -36,8 +46,4 @@ body.navigation-body {
#filter, #search, .list-group-item {
background-color: rgba(0, 0, 0, .25) !important;
}
-}
-
-#lyrics-synced-fade {
- display: none !important;
} \ No newline at end of file
diff --git a/assets/styles.css b/assets/styles.css
index a409a09..0ad2afc 100644
--- a/assets/styles.css
+++ b/assets/styles.css
@@ -2,6 +2,11 @@ iframe {
border: none;
}
+body.native #lyrics-page {
+ top: calc(65px + var(--android-status-bar)) !important;
+ background-color: white !important;
+}
+
* {
user-select: none;
-webkit-user-drag: none;
@@ -121,6 +126,16 @@ iframe {
}
}
+@media (min-height: 64px) {
+ .navigation-item-desktop {
+ display: block;
+ }
+
+ .navigation-item-mobile {
+ display: none;
+ }
+}
+
@media (max-width: 863px) {
#player.bg-white.desktop-player.mobilified .container {
grid-template-columns: 1fr !important;
@@ -177,6 +192,10 @@ iframe {
border-top: 1px solid rgba(0, 0, 0, .25);
}
+ .desktop-title {
+ display: none !important;
+ }
+
iframe#player {
position: fixed !important;
top: unset !important;
@@ -245,22 +264,26 @@ iframe {
}
#ui, #lyrics-page {
- top: var(--android-status-bar) !important;
+ top: calc(var(--android-status-bar) + 49px) !important;
bottom: calc(97px + var(--android-navigation-bar)) !important;
left: 0 !important;
right: 0 !important;
width: 100vw !important;
- height: calc(100vh - 97px - var(--android-navigation-bar) - var(--android-status-bar)) !important;
+ height: calc(100vh - 97px - var(--android-navigation-bar) - var(--android-status-bar) - 49px) !important;
+ }
+
+ #mobile-navbar {
+ display: grid !important;
}
iframe#lyrics-page {
display: block !important;
z-index: 100000 !important;
- top: 96px !important;
+ top: calc(96px + var(--android-status-bar)) !important;
left: 20px !important;
right: 20px !important;
width: calc(100vw - 40px) !important;
- height: calc(100vh - 276px) !important;
+ height: calc(100vh - 276px - var(--android-status-bar) - var(--android-navigation-bar)) !important;
opacity: 0;
pointer-events: none;
transition: opacity 200ms !important;
@@ -269,7 +292,7 @@ iframe {
iframe#lyrics-page.mobile-show {
transition: opacity 200ms linear 500ms !important;
opacity: 1;
- pointer-events: auto !important;
+ pointer-events: none !important;
}
}
@@ -365,12 +388,20 @@ iframe {
}
@media (max-height: 64px) {
+ .navigation-item-desktop {
+ display: none;
+ }
+
+ .navigation-item-mobile {
+ display: block;
+ }
+
#navigation-left, #navigation-version {
display: none !important;
}
#navigation-container {
- grid-template-columns: repeat(6, 1fr) !important;
+ grid-template-columns: repeat(4, 1fr) !important;
display: grid !important;
margin-top: 2px;
}
@@ -450,4 +481,36 @@ body.web {
.modal-dialog {
margin-top: calc(1.75rem + var(--android-status-bar));
margin-bottom: calc(1.75rem + var(--android-navigation-bar));
+}
+
+.has-navigation {
+ padding-top: 32px;
+}
+
+#ui-back-button:hover {
+ opacity: .75;
+}
+
+#ui-back-button:active {
+ opacity: .5;
+}
+
+#filter {
+ background-color: rgba(255, 255, 255, .25) !important;
+}
+
+.mobile-ui.has-navigation {
+ padding-top: 0 !important;
+}
+
+.mobile-ui #ui-navigation {
+ display: none !important;
+}
+
+.mist-stella {
+ display: none !important;
+}
+
+.tooltip {
+ z-index: 99999 !important;
} \ No newline at end of file
diff --git a/desktop/.DS_Store b/desktop/.DS_Store
index 172599e..1d22160 100644
--- a/desktop/.DS_Store
+++ b/desktop/.DS_Store
Binary files differ
diff --git a/desktop/main.js b/desktop/main.js
index 509bd06..1d0b322 100644
--- a/desktop/main.js
+++ b/desktop/main.js
@@ -1,4 +1,4 @@
-const { app, BrowserWindow, shell, ipcMain, session, Notification, nativeImage, Menu } = require('electron');
+const { app, BrowserWindow, shell, ipcMain, session, Notification, nativeImage, Menu, nativeTheme } = require('electron');
const path = require('node:path');
const http = require('http');
const {Client} = require("@xhayper/discord-rpc");
@@ -82,7 +82,6 @@ function updateMenu(mainWindow) {
submenu: [
{
label: "Play/Pause",
- accelerator: "Space",
click: () => {
mainWindow.webContents.executeJavaScript("window.playPause();");
}
@@ -255,18 +254,20 @@ const createWindow = () => {
height: 720,
minWidth: 864,
minHeight: 550,
- frame: process.platform === "darwin" || process.platform === "win32",
+ frame: process.platform === "darwin",
titleBarStyle: "hidden",
- titleBarOverlay: true,
+ titleBarOverlay: {
+ color: nativeTheme.shouldUseDarkColors ? "#222222" : "#ffffff",
+ symbolColor: nativeTheme.shouldUseDarkColors ? "#ffffff" : "#222222",
+ height: 64
+ },
autoHideMenuBar: true,
trafficLightPosition: {
x: 20,
y: 20
},
- //vibrancy: "hud",
vibrancy: "under-window",
- transparent: process.platform === "darwin" || process.platform === "win32",
- backgroundMaterial: "mica",
+ transparent: process.platform === "darwin",
title: "Mist",
type: "textured",
webPreferences: {
diff --git a/includes/.DS_Store b/includes/.DS_Store
index ab60279..fce60f4 100644
--- a/includes/.DS_Store
+++ b/includes/.DS_Store
Binary files differ
diff --git a/includes/check.js b/includes/check.js
new file mode 100644
index 0000000..d5ff55f
--- /dev/null
+++ b/includes/check.js
@@ -0,0 +1,117 @@
+const fs = require('fs');
+const cp = require('child_process');
+const path = require('path');
+const crypto = require('crypto');
+
+const songs = require('../assets/content/songs.json');
+const albums = require('../assets/content/albums.json');
+
+function scandir(dir) {
+ let count = 0;
+
+ function updateScandirDisplay() {
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ process.stdout.write("Scanning... " + count);
+ }
+
+ return new Promise((res, rej) => {
+ const walk = (dir, done) => {
+ let results = [];
+ fs.readdir(dir, function(err, list) {
+ count++;
+ updateScandirDisplay();
+ if (err) return done(err);
+ let pending = list.length;
+
+ if (!pending) return done(null, results);
+ list.forEach(function(file) {
+ count++;
+ updateScandirDisplay();
+ file = path.resolve(dir, file);
+ fs.stat(file, function(err, stat) {
+ if (stat && stat.isDirectory()) {
+ walk(file, function(err, res) {
+ results = results.concat(res);
+ if (!--pending) done(null, results);
+ });
+ } else {
+ results.push(file);
+ if (!--pending) done(null, results);
+ }
+ });
+ });
+ });
+ }
+
+ walk(dir, (err, data) => {
+ if (err) {
+ rej(err);
+ } else {
+ res(data);
+ }
+ })
+ });
+}
+
+(async () => {
+ let list = (await scandir("../assets/content/_")).filter(i => i.endsWith(".flac"));
+ process.stdout.write(" files found\n");
+ let index = 0;
+ console.log("Checking for corrupted files...");
+
+ for (let file of list) {
+ process.stdout.cursorTo(0);
+ process.stdout.write(index + "/" + list.length);
+ let metadata = JSON.parse(cp.execFileSync("ffprobe", ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", file]).toString());
+ if (!metadata['format']['tags']) {
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ console.log(file + ": is corrupted or has no metadata");
+ }
+ index++;
+ }
+
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ console.log("Checking for missing album art...");
+
+ index = 0;
+ for (let id of Object.keys(songs)) {
+ process.stdout.cursorTo(0);
+ process.stdout.write(index + "/" + (Object.keys(songs).length + Object.keys(albums).length));
+ if (fs.existsSync("../assets/content/" + id + ".jpg")) {
+ if (crypto.createHash("md5").update(fs.readFileSync("../assets/content/" + id + ".jpg")).digest("hex") === "fd89a584ae426e72801f2a4c2653ae7a") {
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ console.log(songs[id].title + " (" + id + "): using placeholder album art");
+ }
+ } else {
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ console.log(songs[id].title + " (" + id + "): no album art file");
+ }
+ index++;
+ }
+
+ for (let id of Object.keys(albums)) {
+ process.stdout.cursorTo(0);
+ process.stdout.write(index + "/" + (Object.keys(songs).length + Object.keys(albums).length));
+ if (fs.existsSync("../assets/content/" + id + ".jpg")) {
+ if (crypto.createHash("md5").update(fs.readFileSync("../assets/content/" + id + ".jpg")).digest("hex") === "fd89a584ae426e72801f2a4c2653ae7a") {
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ console.log(albums[id].title + " (" + id + "): using placeholder album art");
+ }
+ } else {
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ console.log(albums[id].title + " (" + id + "): no album art file");
+ }
+ index++;
+ }
+
+ process.stdout.clearLine(null);
+ process.stdout.cursorTo(0);
+ console.log("Done.");
+})(); \ No newline at end of file
diff --git a/includes/process.js b/includes/process.js
index 0c34f94..14dc963 100644
--- a/includes/process.js
+++ b/includes/process.js
@@ -139,7 +139,7 @@ function timeToString(time) {
album: substitute(metadata['format']['tags']['ALBUM'] ?? metadata['format']['tags']['album'] ?? "Unknown album"),
artist: (substitute(metadata['format']['tags']['ARTIST'] ?? metadata['format']['tags']['artist'] ?? "Unknown artist")).replaceAll(";", ", ").replaceAll(" & ", ", ").replaceAll("&", ", ").replaceAll(", and ", ", "),
albumArtist: (substitute(metadata['format']['tags']['album_artist'] ?? metadata['format']['tags']['ARTIST'] ?? metadata['format']['tags']['artist'] ?? "Unknown artist")).replaceAll(";", ", ").replaceAll("&", ", ").replaceAll(", and ", ", "),
- date: parseInt(((metadata['format']['tags']['DATE'] ?? metadata['format']['tags']['date']).split("-")[0]).substring(0, 4)) ?? 0,
+ date: parseInt(((metadata['format']['tags']['DATE'] ?? metadata['format']['tags']['date'] ?? "0").split("-")[0]).substring(0, 4)) ?? 0,
track: parseInt(metadata['format']['tags']['track']) ?? 0,
disc: parseInt(metadata['format']['tags']['disc']) ?? 1,
copyright: metadata['format']['tags']['COPYRIGHT'] ?? metadata['format']['tags']['copyright'] ?? "",
@@ -217,16 +217,16 @@ function timeToString(time) {
console.log("Collecting albums...");
for (let song of Object.keys(songs)) {
- if (Object.values(albums).filter(i => i.title === songs[song].album).length > 0) {
+ if (Object.values(albums).filter(i => i.title === songs[song].album && i.artist === songs[song].albumArtist).length > 0) {
Object.values(albums).filter(i => i.title === songs[song].album)[0].tracks.push(song);
Object.values(albums).filter(i => i.title === songs[song].album)[0].hiRes = Object.values(albums).filter(i => i.title === songs[song].album)[0].hiRes || songs[song].hiRes;
} else {
let albumID = uuid();
if (fs.existsSync("/opt/mist")) {
- fs.copyFileSync("/opt/mist/jpeg/" + song + ".jpg", "/opt/mist/jpeg/" + albumID + ".jpg")
+ if (fs.existsSync("/opt/mist/jpeg/" + song + ".jpg")) fs.copyFileSync("/opt/mist/jpeg/" + song + ".jpg", "/opt/mist/jpeg/" + albumID + ".jpg");
} else {
- fs.copyFileSync("../assets/content/" + song + ".jpg", "../assets/content/" + albumID + ".jpg")
+ if (fs.existsSync("../assets/content/" + song + ".jpg")) fs.copyFileSync("../assets/content/" + song + ".jpg", "../assets/content/" + albumID + ".jpg");
}
albums[albumID] = {
@@ -254,6 +254,7 @@ function timeToString(time) {
}
}
+ console.log("Linking...");
let idList = [...Object.keys(songs), ...Object.keys(albums)];
if (fs.existsSync("/opt/mist")) {
@@ -314,6 +315,18 @@ function timeToString(time) {
album["tracks"] = [...new Set(album["tracks"])].filter(i => songs[i]).sort((a, b) => {
return songs[a]['track'] - songs[b]['track'];
});
+
+ if (fs.existsSync("/opt/mist")) {
+ if (!fs.existsSync("/opt/mist/jpeg/" + albumID + ".jpg")) fs.copyFileSync("../assets/default.jpg", "/opt/mist/jpeg/" + albumID + ".jpg");
+ } else {
+ if (!fs.existsSync("../assets/content/" + albumID + ".jpg")) fs.copyFileSync("../assets/default.jpg", "../assets/content/" + albumID + ".jpg");
+ }
+ }
+
+ for (let albumID of Object.keys(albums)) {
+ if (albums[albumID]["tracks"].length === 0) {
+ delete albums[albumID];
+ }
}
console.log("Writing metadata...");
diff --git a/includes/session.php b/includes/session.php
index 36c5216..e9f9f30 100644
--- a/includes/session.php
+++ b/includes/session.php
@@ -72,12 +72,14 @@ function displayList($list, $hasAlbum = false) { global $albums; global $favorit
<img class="icon" alt="" src="/assets/icons/menu.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-menu-<?= $id ?>-icon">
</span>
<ul class="dropdown-menu">
- <li><a onclick="playNext('<?= $id ?>');" class="dropdown-item" href="#"><img alt="" src="/assets/icons/playnext.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Play next</a></li>
- <li><a onclick="enqueue('<?= $id ?>');" class="dropdown-item" href="#"><img alt="" src="/assets/icons/add.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Add to queue</a></li>
- <li><hr class="dropdown-divider"></hr></li>
- <li><a id="btn-favorite-<?= $id ?>" onclick="<?= in_array($id, $favorites) ? "un" : "" ?>favoriteSong('<?= $id ?>');" class="dropdown-item" href="#"><img id="btn-favorite-<?= $id ?>-icon" alt="" src="/assets/icons/favorite-<?= in_array($id, $favorites) ? "on" : "off" ?>.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;"><span id="btn-favorite-<?= $id ?>-text"><?= in_array($id, $favorites) ? "Remove from favorites" : "Add to favorites" ?></span></a></li>
- <li><a onclick="downloadSong('<?= $id ?>');" class="dropdown-item" href="#"><img alt="" src="/assets/icons/download.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Download</a></li>
- <li><a onclick="getInfo('<?= $id ?>');" class="dropdown-item" href="#"><img alt="" src="/assets/icons/info.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Get info</a></li>
+ <li><a onclick="playNext('<?= $id ?>');" class="dropdown-item" style="cursor: pointer;"><img alt="" src="/assets/icons/playnext.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Play next</a></li>
+ <li><a onclick="enqueue('<?= $id ?>');" class="dropdown-item" style="cursor: pointer;"><img alt="" src="/assets/icons/add.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Add to queue</a></li>
+ <li><hr class="dropdown-divider"></li>
+ <li><a id="btn-favorite-<?= $id ?>" onclick="<?= in_array($id, $favorites) ? "un" : "" ?>favoriteSong('<?= $id ?>');" class="dropdown-item" style="cursor: pointer;"><img id="btn-favorite-<?= $id ?>-icon" alt="" src="/assets/icons/favorite-<?= in_array($id, $favorites) ? "on" : "off" ?>.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;"><span id="btn-favorite-<?= $id ?>-text"><?= in_array($id, $favorites) ? "Remove from favorites" : "Add to favorites" ?></span></a></li>
+ <li><a onclick="downloadSong('<?= $id ?>');" class="dropdown-item" style="cursor: pointer;"><img alt="" src="/assets/icons/download.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Download</a></li>
+ <li><a onclick="getInfo('<?= $id ?>');" class="dropdown-item" style="cursor: pointer;"><img alt="" src="/assets/icons/info.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Get info</a></li>
+ <li><hr class="dropdown-divider"></li>
+ <li><a onclick="navigator.clipboard.writeText('<?= $id ?>');" class="dropdown-item" style="cursor: pointer;"><img alt="" src="/assets/icons/copy.svg" style="pointer-events: none; width: 24px; height: 24px; margin-right: 5px;">Copy ID</a></li>
</ul>
</span>
</div>
@@ -100,7 +102,7 @@ function displayList($list, $hasAlbum = false) { global $albums; global $favorit
if (window.parent.playlist.length === 0) {
window.parent.playSong(id);
} else {
- window.parent.playlist.splice(window.parent.currentPlaylistPosition, 0, id);
+ window.parent.playlist.splice(window.parent.currentPlaylistPosition + 1, 0, id);
}
}
diff --git a/includes/stella.js b/includes/stella.js
new file mode 100644
index 0000000..e38afc5
--- /dev/null
+++ b/includes/stella.js
@@ -0,0 +1,97 @@
+const songs = require('../assets/content/songs.json');
+const cp = require('child_process');
+const zlib = require('zlib');
+const fs = require("fs");
+const crypto = require("crypto");
+
+if (!process.argv[2]) {
+ console.log("Error: Please pass the ID of a song to encode in Stella as a parameter.");
+ return;
+}
+
+if (!songs[process.argv[2]]) {
+ console.log("Error: No song with ID " + process.argv[2] + " could be found.");
+ return;
+}
+
+let song = songs[process.argv[2]];
+console.log("Song: " + song['artist'] + " - " + song['title']);
+
+console.log("Preparing to split stems...");
+cp.execSync("ssh zephyrheights rm -rf /root/StellaTemp", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights mkdir -p /root/StellaTemp", { stdio: "inherit" });
+cp.execSync("scp /opt/mist/flac/" + process.argv[2] + ".flac zephyrheights:/root/StellaTemp/source.flac", { stdio: "inherit" });
+
+console.log("Downsampling to 16bit 44.1kHz...");
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/source.flac -ar 44100 -c:a pcm_s16le /root/StellaTemp/source.wav", { stdio: "inherit" });
+
+console.log("Splitting into 5 stems...");
+cp.execSync("ssh zephyrheights spleeter separate -p spleeter:5stems -b 512k -o /root/StellaTemp/stems -f {instrument}.{codec} /root/StellaTemp/source.flac");
+cp.execSync("ssh zephyrheights mv /root/StellaTemp/stems/* /root/StellaTemp/");
+cp.execSync("ssh zephyrheights rmdir /root/StellaTemp/stems");
+
+console.log("Applying spatial effects to stems...");
+cp.execSync("ssh zephyrheights sox -S /root/StellaTemp/source.wav /root/StellaTemp/hpf.wav sinc 11k -t 1", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/drums.wav -af \"bass=5\" /root/StellaTemp/drums_pre.wav", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/other.wav -af apulsator=hz=0.015 -af aecho=1.0:0.7:20:0.5 /root/StellaTemp/other_pre.wav", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights sox /root/StellaTemp/hpf.wav /root/StellaTemp/hpf_out.wav reverb 30 25 75", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights sox /root/StellaTemp/bass.wav /root/StellaTemp/bass_out.wav reverb 30 25 75", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights sox /root/StellaTemp/drums_pre.wav /root/StellaTemp/drums_out.wav reverb 30 25 75", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights sox /root/StellaTemp/other_pre.wav /root/StellaTemp/other_out.wav reverb 30 25 75", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights sox /root/StellaTemp/piano.wav /root/StellaTemp/piano_out.wav reverb 30 25 75", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights sox /root/StellaTemp/vocals.wav /root/StellaTemp/vocals_out.wav reverb 30 25 75", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/vocals_out.wav /root/StellaTemp/vocals.flac", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/piano_out.wav /root/StellaTemp/piano.flac", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/other_out.wav /root/StellaTemp/other.flac", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/drums_out.wav /root/StellaTemp/drums.flac", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/bass_out.wav /root/StellaTemp/bass.flac", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights ffmpeg -i /root/StellaTemp/hpf_out.wav /root/StellaTemp/hpf.flac", { stdio: "inherit" });
+
+console.log("Exporting...");
+cp.execSync("ssh zephyrheights mkdir /root/StellaTemp/out", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights mv /root/StellaTemp/vocals.flac /root/StellaTemp/piano.flac /root/StellaTemp/other.flac /root/StellaTemp/drums.flac /root/StellaTemp/bass.flac /root/StellaTemp/hpf.flac /root/StellaTemp/out", { stdio: "inherit" });
+cp.execSync("mkdir tmp", { stdio: "inherit" });
+cp.execSync("scp zephyrheights:/root/StellaTemp/out/* tmp", { stdio: "inherit" });
+cp.execSync("ssh zephyrheights rm -rf /root/StellaTemp", { stdio: "inherit" });
+
+console.log("Preparing Stella file...");
+
+let files = {
+ bass: zlib.deflateRawSync(fs.readFileSync("tmp/bass.flac")),
+ drums: zlib.deflateRawSync(fs.readFileSync("tmp/drums.flac")),
+ hpf: zlib.deflateRawSync(fs.readFileSync("tmp/hpf.flac")),
+ other: zlib.deflateRawSync(fs.readFileSync("tmp/other.flac")),
+ piano: zlib.deflateRawSync(fs.readFileSync("tmp/piano.flac")),
+ vocals: zlib.deflateRawSync(fs.readFileSync("tmp/vocals.flac")),
+}
+
+let magic = Buffer.from("00ffff4f00000100", "hex");
+let metadata = Buffer.from(zlib.deflateRawSync(JSON.stringify({
+ version: "1.0",
+ id: crypto.randomBytes(16).toString("base64").replace(/[^a-zA-Z\d]/g, "").toUpperCase().substring(0, 10),
+ stems: {
+ bass: [512, files.bass.length],
+ drums: [512 + files.bass.length, files.drums.length],
+ hpf: [512 + files.bass.length + files.drums.length, files.hpf.length],
+ other: [512 + files.bass.length + files.drums.length + files.hpf.length, files.other.length],
+ piano: [512 + files.bass.length + files.drums.length + files.hpf.length + files.other.length, files.piano.length],
+ vocals: [512 + files.bass.length + files.drums.length + files.hpf.length + files.other.length + files.piano.length, files.vocals.length],
+ }
+})));
+let padding = Buffer.from("00".repeat(504 - metadata.length), "hex");
+
+let header = Buffer.concat([magic, metadata, padding]);
+if (header.length !== 512) {
+ console.log("Error: Invalid header length.");
+ return;
+}
+
+let file = Buffer.concat([header, files.bass, files.drums, files.hpf, files.other, files.piano, files.vocals]);
+
+console.log("Writing Stella file...");
+fs.writeFileSync("tmp/export.stella", file);
+
+console.log("Adding to server...");
+fs.copyFileSync("tmp/export.stella", "/opt/mist/jpeg/" + process.argv[2] + ".stella");
+if (!fs.existsSync("../assets/content/" + process.argv[2] + ".stella")) fs.symlinkSync("/opt/mist/jpeg/" + process.argv[2] + ".stella", "../assets/content/" + process.argv[2] + ".stella");
+fs.rmSync("tmp", { recursive: true }); \ No newline at end of file
diff --git a/includes/watcher.js b/includes/watcher.js
index b96c93d..d9d74fc 100644
--- a/includes/watcher.js
+++ b/includes/watcher.js
@@ -3,7 +3,7 @@ const fs = require('fs');
let lastUpdate = 0;
watch('..', { recursive: true }, function(evt, name) {
- if (name.includes("includes/users") || name.includes("includes/tokens") || name.includes("assets/content")) return;
+ if (name.includes("includes/users") || name.includes("includes/tokens") || name.includes("includes/app.json") || name.includes("assets/content")) return;
if (new Date().getTime() - lastUpdate > 60000) {
let buildFile = fs.readFileSync("/opt/spotify/build.txt").toString().trim();
diff --git a/manifest.json b/manifest.json
index 02c7a38..2f228bf 100644
--- a/manifest.json
+++ b/manifest.json
@@ -12,8 +12,8 @@
},
"protocol_handlers": [
{
- "protocol": "mist",
- "url": "/api/protocol.php?q=%s"
+ "protocol": "web+mist",
+ "url": "/app/protocol.php?q=%s"
}
],
"scope": "/app/",
diff --git a/normalizer/normalizer.html b/normalizer/normalizer.html
new file mode 100755
index 0000000..d166c65
--- /dev/null
+++ b/normalizer/normalizer.html
@@ -0,0 +1,171 @@
+<script type="text/javascript">
+</script>
+<table>
+
+<tr>
+<th>
+Not normalized
+</th>
+<th>
+Normalized
+</th>
+<th>
+Computed gain
+</th>
+</tr>
+
+<tr>
+<td>
+<audio controls> <source src="sample-a.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<audio id="sample-a-n" controls> <source src="sample-a.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<div id="sample-a-d"></div>
+</td>
+</tr>
+
+<tr>
+<td>
+<audio controls> <source src="sample-b.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<audio id="sample-b-n" controls> <source src="sample-b.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<div id="sample-b-d"></div>
+</td>
+</tr>
+
+<tr>
+<td>
+<audio controls> <source src="sample-c.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<audio id="sample-c-n" controls> <source src="sample-c.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<div id="sample-c-d"></div>
+</td>
+</tr>
+
+<tr>
+<td>
+<audio controls> <source src="sample-d.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<audio id="sample-d-n" controls> <source src="sample-d.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<div id="sample-d-d"></div>
+</td>
+</tr>
+
+<tr>
+<td>
+<audio controls> <source src="sample-e.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<audio id="sample-e-n" controls> <source src="sample-e.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<div id="sample-e-d"></div>
+</td>
+</tr>
+
+<tr>
+<td>
+<audio controls> <source src="sample-f.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<audio id="sample-f-n" controls> <source src="sample-f.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<div id="sample-f-d"></div>
+</td>
+</tr>
+
+<tr>
+<td>
+<audio controls> <source src="sample-g.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<audio id="sample-g-n" controls> <source src="sample-g.mp3" type="audio/mpeg">Audio element not supported</audio>
+</td>
+<td>
+<div id="sample-g-d"></div>
+</td>
+</tr>
+
+</table>
+<script type="text/javascript">
+ function start() {
+ var audioCtx = new AudioContext();
+
+// http://wiki.hydrogenaud.io/index.php?title=ReplayGain_specification
+// TODO: do the loudness filtering (Butterworth, Yulewalk) IIR filters
+
+ function normalizedAudioElement(name) {
+ var audioElem = document.getElementById(name + "-n");
+ var src = audioCtx.createMediaElementSource(audioElem);
+ var gainNode = audioCtx.createGain();
+ gainNode.gain.value = 1.0;
+
+ audioElem.addEventListener("play", function() {
+ src.connect(gainNode);
+ gainNode.connect(audioCtx.destination);
+ }, true);
+ audioElem.addEventListener("pause", function() {
+ // disconnect the nodes on pause, otherwise all nodes always run
+ src.disconnect(gainNode);
+ gainNode.disconnect(audioCtx.destination);
+ }, true);
+ fetch(name + ".mp3")
+ .then(function(res) { return res.arrayBuffer(); })
+ .then(function(buf) {
+ return audioCtx.decodeAudioData(buf);
+ }).then(function(decodedData) {
+ var decodedBuffer = decodedData.getChannelData(0);
+ var sliceLen = Math.floor(decodedData.sampleRate * 0.05);
+ var averages = [];
+ var sum = 0.0;
+ for (var i = 0; i < decodedBuffer.length; i++) {
+ sum += decodedBuffer[i] ** 2;
+ if (i % sliceLen === 0) {
+ sum = Math.sqrt(sum / sliceLen);
+ averages.push(sum);
+ sum = 0;
+ }
+ }
+ // Ascending sort of the averages array
+ averages.sort(function(a, b) { return a - b; });
+ // Take the average at the 95th percentile
+ var a = averages[Math.floor(averages.length * 0.95)];
+
+ var gain = 1.0 / a;
+ // Perform some clamping
+ // gain = Math.max(gain, 0.02);
+ // gain = Math.min(gain, 100.0);
+
+ // ReplayGain uses pink noise for this one one but we just take
+ // some arbitrary value... we're no standard
+ // Important is only that we don't output on levels
+ // too different from other websites
+ gain = gain / 10.0;
+ console.log("gain determined", name, a, gain);
+ gainNode.gain.value = gain;
+ var gainTextElem = document.getElementById(name + "-d");
+ gainTextElem.textContent = gain.toPrecision(4);
+ });
+ }
+
+ normalizedAudioElement("sample-a");
+ normalizedAudioElement("sample-b");
+ normalizedAudioElement("sample-c");
+ normalizedAudioElement("sample-d");
+ normalizedAudioElement("sample-e");
+ normalizedAudioElement("sample-f");
+ normalizedAudioElement("sample-g");
+ }
+</script>
diff --git a/normalizer/sample-a.mp3 b/normalizer/sample-a.mp3
new file mode 100755
index 0000000..6b6041e
--- /dev/null
+++ b/normalizer/sample-a.mp3
Binary files differ
diff --git a/normalizer/sample-b.mp3 b/normalizer/sample-b.mp3
new file mode 100755
index 0000000..bb8c766
--- /dev/null
+++ b/normalizer/sample-b.mp3
Binary files differ
diff --git a/normalizer/sample-c.mp3 b/normalizer/sample-c.mp3
new file mode 100755
index 0000000..bc488f1
--- /dev/null
+++ b/normalizer/sample-c.mp3
Binary files differ
diff --git a/normalizer/sample-d.mp3 b/normalizer/sample-d.mp3
new file mode 100755
index 0000000..b51df1c
--- /dev/null
+++ b/normalizer/sample-d.mp3
Binary files differ
diff --git a/normalizer/sample-e.mp3 b/normalizer/sample-e.mp3
new file mode 100755
index 0000000..85ef615
--- /dev/null
+++ b/normalizer/sample-e.mp3
Binary files differ
diff --git a/normalizer/sample-f.mp3 b/normalizer/sample-f.mp3
new file mode 100755
index 0000000..17e5e96
--- /dev/null
+++ b/normalizer/sample-f.mp3
Binary files differ
diff --git a/normalizer/sample-g.mp3 b/normalizer/sample-g.mp3
new file mode 100755
index 0000000..e610b33
--- /dev/null
+++ b/normalizer/sample-g.mp3
Binary files differ
diff --git a/version b/version
index 8cfaa0a..308b6fa 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.3.2|Public Beta \ No newline at end of file
+1.6.2 \ No newline at end of file