summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/albums.php104
-rw-r--r--app/explore.php118
-rw-r--r--app/favorites.php14
-rw-r--r--app/index.php551
-rw-r--r--app/listing.php213
-rw-r--r--app/lyrics.php158
-rw-r--r--app/navigation.php66
-rw-r--r--app/player-mobile.php79
-rw-r--r--app/player.php76
-rw-r--r--app/settings.php68
-rw-r--r--app/songs.php4
11 files changed, 1451 insertions, 0 deletions
diff --git a/app/albums.php b/app/albums.php
new file mode 100644
index 0000000..a5fcfe5
--- /dev/null
+++ b/app/albums.php
@@ -0,0 +1,104 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>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">
+ <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/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform">
+ <script>
+ if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("native-css").disabled = false;
+ document.body.classList.remove("crossplatform");
+ }
+ </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;
+
+ $albums = array_filter($albums, function ($i) {
+ global $library;
+ return in_array($i, $library);
+ }, ARRAY_FILTER_USE_KEY);
+
+ uasort($albums, function ($a, $b) {
+ return strcmp($a["title"], $b["title"]);
+ });
+
+ uasort($albums, function ($a, $b) {
+ return strcmp($a["artist"], $b["artist"]);
+ });
+
+ foreach ($albums as $id => $album): ?>
+ <a id="album-<?= $id ?>" data-item-track="<?= $album["title"] ?>" data-item-artist="<?= $album["artist"] ?>" href="listing.php?a=<?= $id ?>" style="padding: 10px; color: inherit; text-decoration: inherit;" class="album">
+ <img class="album-list-art" alt="" src="/assets/content/<?= $id ?>.jpg" style="width: 100%; aspect-ratio: 1; border-radius: 5px; margin-bottom: 5px;">
+ <div class="album-list-item" style="max-width: calc(80vw / 5); white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $album["title"] ?></div>
+ <div class="album-list-item" style="max-width: calc(80vw / 5); opacity: .5; white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $album["artist"] ?></div>
+ </a>
+ <?php endforeach; ?>
+ </div>
+ <?php if (count($albums) === 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>
+ </div>
+ <?php endif; ?>
+ <div id="search-results" style="display: none; grid-template-columns: repeat(5, 1fr);"></div>
+ </div>
+
+ <script>
+ let items = Array.from(document.getElementsByClassName("album")).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 = "grid";
+ document.getElementById("album-grid").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("album-grid").style.display = "grid";
+ }
+ }
+ </script>
+
+ <br><br>
+</body>
+</html> \ No newline at end of file
diff --git a/app/explore.php b/app/explore.php
new file mode 100644
index 0000000..a51731b
--- /dev/null
+++ b/app/explore.php
@@ -0,0 +1,118 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>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">
+ <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/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform">
+ <script>
+ if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("native-css").disabled = false;
+ document.body.classList.remove("crossplatform");
+ }
+ </script>
+ <div class="container">
+ <br>
+ <h2 style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Explore<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;
+
+ uasort($albums, function ($a, $b) {
+ return strcmp($a["title"], $b["title"]);
+ });
+
+ uasort($albums, function ($a, $b) {
+ return strcmp($a["artist"], $b["artist"]);
+ });
+
+ foreach ($albums as $id => $album): ?>
+ <a id="album-<?= $id ?>" data-item-track="<?= $album["title"] ?>" data-item-artist="<?= $album["artist"] ?>" href="listing.php?a=<?= $id ?>" style="padding: 10px; color: inherit; text-decoration: inherit;" class="album">
+ <img class="album-list-art" alt="" src="/assets/content/<?= $id ?>.jpg" style="width: 100%; aspect-ratio: 1; border-radius: 5px; margin-bottom: 5px;">
+ <div class="album-list-item" style="max-width: calc(80vw / 5); white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $album["title"] ?></div>
+ <div class="album-list-item" style="max-width: calc(80vw / 5); opacity: .5; white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $album["artist"] ?></div>
+ </a>
+ <?php endforeach; ?>
+ </div>
+ <div id="search-results" style="display: none; grid-template-columns: repeat(5, 1fr);"></div>
+ <?php global $songs; displayList($songs); ?>
+ <div class="list-group" style="margin-left: 10px; margin-top: 20px; display: none;" id="search-results-2"></div>
+ </div>
+
+ <script>
+ let items = Array.from(document.getElementsByClassName("album")).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
+ }
+ ]
+ });
+
+ let items2 = Array.from(document.getElementsByClassName("track")).map(i => { return { title: i.getAttribute("data-item-track"), artist: i.getAttribute("data-item-artist"), id: i.id } });
+
+ const fuse2 = new Fuse(items2, {
+ 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 = "grid";
+ document.getElementById("album-grid").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;
+ }
+
+ document.getElementById("search-results-2").style.display = "flex";
+ document.getElementById("main-list").style.display = "none";
+
+ let results2 = fuse2.search(query);
+ document.getElementById("search-results-2").innerHTML = "";
+
+ for (let result of results2) {
+ document.getElementById("search-results-2").innerHTML += document.getElementById(result.item.id).outerHTML;
+ }
+ } else {
+ document.getElementById("search-results").style.display = "none";
+ document.getElementById("album-grid").style.display = "grid";
+ document.getElementById("search-results-2").style.display = "none";
+ document.getElementById("main-list").style.display = "flex";
+ }
+ }
+ </script>
+
+ <br><br>
+</body>
+</html> \ No newline at end of file
diff --git a/app/favorites.php b/app/favorites.php
new file mode 100644
index 0000000..9690523
--- /dev/null
+++ b/app/favorites.php
@@ -0,0 +1,14 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php";
+global $songs; global $favorites;
+
+$hasAlbum = false;
+$favoritesList = true;
+$list = [];
+
+foreach ($favorites as $id) {
+ $list[$id] = $songs[$id];
+}
+
+require_once "listing.php"; \ No newline at end of file
diff --git a/app/index.php b/app/index.php
new file mode 100644
index 0000000..e6d93ff
--- /dev/null
+++ b/app/index.php
@@ -0,0 +1,551 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFILE; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>Mist</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/shortcuts.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)">
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
+ <meta name="apple-mobile-web-app-status-bar" content="#ffffff" media="(prefers-color-scheme: light)">
+ <meta name="apple-mobile-web-app-status-bar" content="#000000" media="(prefers-color-scheme: dark)">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" media="(prefers-color-scheme: dark)">
+ <meta name="apple-mobile-web-app-status-bar-style" content="white-translucent" media="(prefers-color-scheme: light)">
+ <meta name="description" content="Mist Audio Player">
+</head>
+<body>
+ <script>
+ if (location.hash.trim() === "") location.hash = "#/albums";
+
+ if (window.MistNative) {
+ MistNative.version("<?= explode("|", trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")))[0] ?>", "<?= trim(file_get_contents("/opt/spotify/build.txt")) ?>");
+ MistNative.userInfo(`<?= str_replace("`", "\\`", json_encode($_PROFILE)) ?>`);
+ }
+ </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>
+ </div>
+
+ <iframe title="Player" id="player" src="player.php" style="position: fixed; top: 0; left: 320px; right: 0; width: calc(100vw - 320px); height: 64px; border-bottom: 1px solid rgba(0, 0, 0, .25); z-index: 9999;"></iframe>
+ <iframe title="Navigation" id="navigation" src="navigation.php" style="position: fixed; top: 0; bottom: 0; left: 0; height: 100vh; width: 320px; z-index: 9999;"></iframe>
+ <iframe title="UI" id="ui" style="position: fixed; top: 65px; bottom: 0; left: 320px; height: calc(100vh - 64px); width: calc(100vw - 320px); z-index: 9999;"></iframe>
+ <iframe title="Lyrics" id="lyrics-page" src="lyrics.php" style="display: none; position: fixed; top: 64px; bottom: 0; left: 320px; height: calc(100vh - 64px); width: calc(100vw - 320px); z-index: 9999;"></iframe>
+ <div id="player-mobile-container" style="background-color: white; position: fixed; left: 0; right: 0; height: 100vh; bottom: -100vh; transition: bottom 200ms; z-index: 99999;">
+ <iframe title="Mobile player" style="background: #ddd; width: 100%; height: 100%;" src="player-mobile.php" id="player-mobile"></iframe>
+ </div>
+
+ <script>
+ window.playlist = [];
+
+ window.showLyrics = () => {
+ location.hash = "#/lyrics";
+ 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");
+ }
+
+ function openUI(name) {
+ hideMobilePlayer();
+ location.hash = "#/" + name;
+ document.getElementById("lyrics-page").style.display = "none";
+ document.getElementById("ui").style.display = "";
+ document.getElementById("ui").src = 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");
+ }
+
+ let name = location.hash.split("/")[1];
+
+ if (name === "lyrics") {
+ document.getElementById("ui").src = "albums.php";
+ } else if (name === "albums" && location.hash.split("/")[2]) {
+ document.getElementById("ui").src = "listing.php?a=" + location.hash.split("/")[2];
+ } else {
+ document.getElementById("ui").src = name + ".php";
+ }
+
+ document.getElementById("navigation").onload = () => {
+ 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");
+ }
+
+ let loadedPlayers = 0;
+
+ document.getElementById("player").onload = document.getElementById("player-mobile").onload = () => {
+ loadedPlayers++;
+ if (loadedPlayers === 2) continueLoading();
+ }
+
+ window.onresize = window.onload = () => {
+ if (window.innerWidth <= 863) {
+ document.getElementById("player").contentDocument.getElementById("player").classList.add("mobilified");
+ } else {
+ document.getElementById("player").contentDocument.getElementById("player").classList.remove("mobilified");
+ }
+ }
+
+ function continueLoading() {
+ window.playerDocument = document.getElementById("player").contentDocument;
+ window.playerDocumentMobile = document.getElementById("player-mobile").contentDocument;
+
+ if (!localStorage.getItem("data-saving")) {
+ localStorage.setItem("data-saving", "false");
+ }
+
+ 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;
+ }
+
+ playerDocumentMobile.getElementById("seekbar-container").onclick = (e) => {
+ playerDocument.getElementById("player-audio").currentTime = (e.offsetX / playerDocumentMobile.getElementById("seekbar-container").clientWidth) * playerDocument.getElementById("player-audio").duration;
+ }
+
+ function parseTime(subject, max) {
+ let minutesLength = Math.ceil(max / 60).toString().length;
+
+ let minutes = Math.floor(subject / 60);
+ let seconds = Math.floor(subject - (minutes * 60));
+
+ let minutesStr = "0".repeat(minutesLength).substring(0, minutesLength - minutes.toString().length) + minutes.toString();
+ let secondsStr = "00".substring(0, 2 - seconds.toString().length) + seconds.toString();
+
+ return minutesStr + ":" + secondsStr;
+ }
+
+ playerDocument.getElementById("player-audio").onended = () => {
+ next();
+ }
+
+ window.stop = () => {
+ document.title = "Mist";
+ window.currentSong = null;
+ window.currentSongID = null;
+ window.currentPlaylistID = null;
+ window.playlist = [];
+
+ document.getElementById("player").contentWindow.location.reload();
+ document.getElementById("player").onload = () => {
+ window.playerDocument = document.getElementById("player").contentDocument;
+
+ 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();
+ }
+ }
+
+ document.getElementById("player-mobile").contentWindow.location.reload();
+ document.getElementById("player-mobile").onload = () => {
+ window.playerDocumentMobile = document.getElementById("player-mobile").contentDocument;
+ }
+
+ hideMobilePlayer();
+ }
+
+ window.showMobilePlayer = () => {
+ if (window.currentSongID !== null) {
+ document.getElementById("player-mobile-container").style.bottom = "0";
+ document.getElementById("lyrics-page").classList.add("mobile-show");
+ }
+ }
+
+ window.hideMobilePlayer = () => {
+ document.getElementById("player-mobile-container").style.bottom = "-100vh";
+ document.getElementById("lyrics-page").classList.remove("mobile-show");
+ }
+
+ document.getElementById("player-mobile-container").onclick = (e) => {
+ if (e.target.id === "player-mobile-container") {
+ hideMobilePlayer();
+ }
+ }
+
+ window.currentPlaylistPosition = 0;
+
+ window.next = () => {
+ if (window.repeat) {
+ playlist.push(playlist[currentPlaylistPosition]);
+ }
+
+ if (playlist[currentPlaylistPosition + 1]) {
+ playSong(playlist[currentPlaylistPosition + 1], "keep");
+ currentPlaylistPosition++;
+ } else {
+ stop();
+ }
+ }
+
+ window.previous = () => {
+ if (playlist[currentPlaylistPosition - 1]) {
+ playSong(playlist[currentPlaylistPosition - 1], "keep");
+ currentPlaylistPosition--;
+ } else {
+ stop();
+ }
+ }
+
+ 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 updateDisplay() {
+ if (playerDocument.getElementById("player-audio").paused) {
+ document.title = "Mist";
+ } else if (currentSong) {
+ document.title = currentSong.artist + " — " + currentSong.title;
+ } else {
+ document.title = "Mist";
+ }
+
+ playerDocument.getElementById("info").style.display = "grid";
+ playerDocument.getElementById("cover").style.display = "none";
+
+ document.getElementById("player").contentWindow.navigator.mediaSession.playbackState = playerDocument.getElementById("player-audio").paused ? "paused" : "playing";
+ let state = {
+ duration: isFinite(playerDocument.getElementById("player-audio").duration) ? playerDocument.getElementById("player-audio").duration : 0,
+ position: playerDocument.getElementById("player-audio").currentTime,
+ playbackRate: 1
+ }
+ document.getElementById("player").contentWindow.navigator.mediaSession.setPositionState(state);
+
+ document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("play", () => {
+ playPause();
+ });
+ document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("pause", () => {
+ playPause();
+ });
+ document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("stop", () => {
+ stop();
+ });
+ document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("seekbackward", (e) => {
+ let time = e.seekOffset ?? 10;
+
+ if (playerDocument.getElementById("player-audio").currentTime >= time) {
+ playerDocument.getElementById("player-audio").currentTime -= time;
+ } else {
+ playerDocument.getElementById("player-audio").currentTime = 0;
+ }
+ });
+ 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;
+ } else {
+ next();
+ }
+ });
+ document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("seekto", (e) => {
+ if (e.seekTime) {
+ playerDocument.getElementById("player-audio").currentTime = e.seekTime;
+ }
+ });
+ document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("previoustrack", () => {
+ if (playlist[currentPlaylistPosition - 1]) {
+ previous();
+ } else {
+ playerDocument.getElementById("player-audio").currentTime = 0;
+ }
+ });
+ document.getElementById("player").contentWindow.navigator.mediaSession.setActionHandler("nexttrack", () => {
+ next();
+ });
+
+ if (currentPlaylistPosition === 0) {
+ playerDocument.getElementById("btn-previous").classList.add("disabled");
+ playerDocumentMobile.getElementById("btn-previous").classList.add("disabled");
+ } else {
+ playerDocument.getElementById("btn-previous").classList.remove("disabled");
+ playerDocumentMobile.getElementById("btn-previous").classList.remove("disabled");
+ }
+
+ if (currentPlaylistPosition === playlist.length - 1) {
+ playerDocument.getElementById("btn-next").classList.add("disabled");
+ playerDocumentMobile.getElementById("btn-next").classList.add("disabled");
+ } else {
+ playerDocument.getElementById("btn-next").classList.remove("disabled");
+ playerDocumentMobile.getElementById("btn-next").classList.remove("disabled");
+ }
+
+ if (isFinite(playerDocument.getElementById("player-audio").duration)) {
+ playerDocument.getElementById("elapsed-time").innerText = playerDocumentMobile.getElementById("elapsed-time").innerText = parseTime(playerDocument.getElementById("player-audio").currentTime, playerDocument.getElementById("player-audio").duration);
+ playerDocument.getElementById("remaining-time").innerText = playerDocumentMobile.getElementById("remaining-time").innerText = "-" + parseTime(playerDocument.getElementById("player-audio").duration - playerDocument.getElementById("player-audio").currentTime, playerDocument.getElementById("player-audio").duration);
+ } else {
+ playerDocument.getElementById("elapsed-time").innerText = playerDocumentMobile.getElementById("elapsed-time").innerText = parseTime(0, currentSong.length);
+ playerDocument.getElementById("remaining-time").innerText = playerDocumentMobile.getElementById("remaining-time").innerText = "-" + parseTime(currentSong.length, currentSong.length);
+ }
+
+ if (playerDocument.getElementById("player-audio").paused) {
+ playerDocument.getElementById("btn-play-icon").src = playerDocumentMobile.getElementById("btn-play-icon").src = "/assets/icons/play.svg";
+ } else {
+ 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).toFixed(1) + " 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 = "inline";
+ playerDocument.getElementById("badge-cd").style.display = "inline";
+ playerDocument.getElementById("badge-hires").style.display = "none";
+ playerDocumentMobile.getElementById("badge-lossy").style.display = "inline";
+ 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).toFixed(1) + " 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;
+ playerDocument.getElementById("album").innerText = playerDocumentMobile.getElementById("album").innerText = window.currentSong.album;
+ playerDocument.getElementById("album-art").src = playerDocumentMobile.getElementById("album-art").src = "/assets/content/" + window.currentSongID + ".jpg";
+ playerDocumentMobile.getElementById("album-art-bg").style.backgroundImage = "url('/assets/content/" + window.currentSongID + ".jpg')";
+ }
+
+ if (isFinite(playerDocument.getElementById("player-audio").duration)) {
+ playerDocument.getElementById("seekbar").style.width = playerDocumentMobile.getElementById("seekbar").style.width = ((playerDocument.getElementById("player-audio").currentTime / playerDocument.getElementById("player-audio").duration) * 100) + "%";
+ } else {
+ playerDocument.getElementById("seekbar").style.width = playerDocumentMobile.getElementById("seekbar").style.width = "0";
+ }
+ }
+
+ window.playPause = () => {
+ if (playlist.length === 0) return;
+
+ if (playerDocument.getElementById("player-audio").paused) {
+ playerDocument.getElementById("player-audio").play();
+ } else {
+ playerDocument.getElementById("player-audio").pause();
+ }
+ }
+
+ window.redownloadFavorite = async () => {
+ document.getElementById("loading-text").innerText = "Downloading favorites...";
+ window.favorites = await (await fetch("/api/getFavorites.php")).json();
+ }
+
+ window.redownloadLibrary = async () => {
+ document.getElementById("loading-text").innerText = "Downloading library...";
+ window.favorites = await (await fetch("/api/getLibrary.php")).json();
+ }
+
+ (async () => {
+ document.getElementById("loading-text").innerText = "Downloading list of songs...";
+ window.songs = await (await fetch("/assets/content/songs.json")).json();
+
+ document.getElementById("loading-text").innerText = "Downloading list of albums...";
+ window.albums = await (await fetch("/assets/content/albums.json")).json();
+
+ document.getElementById("loading-text").innerText = "Downloading favorites...";
+ window.favorites = await (await fetch("/api/getFavorites.php")).json();
+
+ document.getElementById("loading-text").innerText = "Saving database...";
+ await localforage.setItem("albums", window.albums);
+ await localforage.setItem("songs", window.songs);
+ await localforage.setItem("favorites", window.favorites);
+
+ document.getElementById("loading-text").innerText = "Done loading.";
+ document.getElementById("loading").style.display = "none";
+ })();
+
+ window.currentSong = null;
+ window.currentSongID = null;
+ window.preloaded = {};
+ window.shuffle = false;
+ window.repeat = false;
+
+ window.toggleRepeat = () => {
+ window.repeat = !window.repeat;
+
+ if (window.repeat) {
+ playerDocument.getElementById("btn-repeat-icon").src = playerDocumentMobile.getElementById("btn-repeat-icon").src = "/assets/icons/repeat-on.svg";
+ } else {
+ playerDocument.getElementById("btn-repeat-icon").src = playerDocumentMobile.getElementById("btn-repeat-icon").src = "/assets/icons/repeat-off.svg";
+ }
+ }
+
+ function shuffleArray(array) {
+ let currentIndex = array.length, temporaryValue, randomIndex;
+
+ while (0 !== currentIndex) {
+ randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex -= 1;
+
+ temporaryValue = array[currentIndex];
+ array[currentIndex] = array[randomIndex];
+ array[randomIndex] = temporaryValue;
+ }
+
+ return array;
+ }
+
+ window.toggleShuffle = () => {
+ window.shuffle = !window.shuffle;
+
+ if (window.shuffle) {
+ window.playlist = shuffleArray(window.playlist);
+ playerDocument.getElementById("btn-shuffle-icon").src = playerDocumentMobile.getElementById("btn-shuffle-icon").src = "/assets/icons/shuffle-on.svg";
+ } else {
+ if (currentPlaylistID) {
+ if (currentPlaylistID === "favorites") {
+ window.playlist = favorites;
+ } else if (currentPlaylistID.startsWith("album:")) {
+ window.playlist = albums[currentPlaylistID.substring(6)].tracks;
+ } else if (currentPlaylistID !== "keep") {
+ window.playlist = [currentSongID];
+ }
+ } else {
+ window.playlist = [currentSongID];
+ }
+
+ playerDocument.getElementById("btn-shuffle-icon").src = playerDocumentMobile.getElementById("btn-shuffle-icon").src = "/assets/icons/shuffle-off.svg";
+ }
+ }
+
+ window.shuffleList = (playlistID) => {
+ window.shuffle = true;
+ playerDocument.getElementById("btn-shuffle-icon").src = playerDocumentMobile.getElementById("btn-shuffle-icon").src = "/assets/icons/shuffle-on.svg";
+
+ if (playlistID) {
+ if (playlistID === "favorites") {
+ window.currentPlaylistID = playlistID;
+ window.playlist = favorites;
+ } else if (playlistID.startsWith("album:")) {
+ window.currentPlaylistID = playlistID;
+ window.playlist = albums[playlistID.substring(6)].tracks;
+ } else if (playlistID !== "keep") {
+ window.playlist = [];
+ }
+ } else {
+ window.currentPlaylistID = null;
+ window.playlist = [];
+ }
+
+ window.playlist = shuffleArray(window.playlist);
+ window.playSong(window.playlist[0], "keep");
+ }
+
+ window.currentPlaylistID = null;
+
+ window.playSong = async (id, playlistID) => {
+ playerDocument.getElementById("player-audio").pause();
+ playerDocument.getElementById("player-audio").currentTime = 0;
+
+ if (playlistID) {
+ if (playlistID === "favorites") {
+ window.currentPlaylistID = playlistID;
+ window.playlist = favorites;
+ window.currentPlaylistPosition = favorites.indexOf(id) ?? 0;
+ } else if (playlistID.startsWith("album:")) {
+ window.currentPlaylistID = playlistID;
+ window.playlist = albums[playlistID.substring(6)].tracks;
+ window.currentPlaylistPosition = albums[playlistID.substring(6)].tracks.indexOf(id) ?? 0;
+ } else if (playlistID !== "keep") {
+ window.playlist = [id];
+ window.currentPlaylistPosition = 0;
+ }
+ } else {
+ window.currentPlaylistID = null;
+ window.playlist = [id];
+ window.currentPlaylistPosition = 0;
+ }
+
+ window.currentSong = songs[id];
+ window.currentSongID = id;
+ updateDisplay();
+
+ 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" }));
+ }
+ }
+
+ cleanupPreload();
+ preloadMore();
+
+ playerDocument.getElementById("player-audio").src = window.preloaded[id];
+ playerDocument.getElementById("player-audio").play();
+
+ if (window.MistNative && localStorage.getItem("desktop-notification") === "true") {
+ window.MistNative.notification(currentSong, await (async function() {
+ let blob = await fetch("/assets/content/" + currentSongID + ".jpg").then(r => r.blob());
+ return await new Promise(resolve => {
+ let reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.readAsDataURL(blob);
+ });
+ })());
+ }
+
+ document.getElementById("player").contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
+ title: currentSong.title,
+ artist: currentSong.artist,
+ album: currentSong.album,
+ artwork: [
+ { src: location.protocol + "//" + location.host + '/assets/content/' + currentSongID + '.jpg', sizes: playerDocument.getElementById("album-art").naturalWidth + "x" + playerDocument.getElementById("album-art").naturalHeight, type: 'image/jpeg' },
+ ]
+ });
+ }
+ }
+
+ async function preloadMore() {
+ for (let i = 1; i <= 10; i++) {
+ if (playlist[currentPlaylistPosition + i]) {
+ let id = playlist[currentPlaylistPosition + i];
+
+ 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" }));
+ }
+ }
+ }
+ }
+
+ cleanupPreload();
+ }
+
+ function cleanupPreload() {
+ let keys = Object.keys(window.preloaded).slice(-20);
+
+ for (let key of Object.keys(window.preloaded)) {
+ if (!keys.includes(key)) {
+ URL.revokeObjectURL(window.preloaded[key]);
+ delete window.preloaded[key];
+ }
+ }
+ }
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/app/listing.php b/app/listing.php
new file mode 100644
index 0000000..8ea25d4
--- /dev/null
+++ b/app/listing.php
@@ -0,0 +1,213 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFILE; global $library;
+
+if (isset($list)) {
+ $presetList = true;
+} else {
+ $presetList = false;
+}
+
+if (!$presetList) {
+ global $albums; global $songs; global $favorites;
+
+ if (isset($_GET["a"]) && isset($albums[$_GET["a"]])) {
+ $hasAlbum = true;
+ $list = [];
+
+ foreach ($albums[$_GET["a"]]["tracks"] as $id) {
+ $list[$id] = $songs[$id];
+ }
+ } else {
+ $hasAlbum = false;
+ $list = $songs;
+
+ foreach ($albums as $id => $album) {
+ foreach ($album["tracks"] as $track) {
+ $list[$track]["_albumID"] = $id;
+ }
+ }
+
+ uasort($list, function ($a, $b) {
+ return strcmp($a["title"], $b["title"]);
+ });
+
+ $list = array_filter($list, function ($i) {
+ global $library;
+ return in_array($i["_albumID"], $library);
+ });
+ }
+}
+
+?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>listing</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/shortcuts.js"></script>
+ <script src="/assets/fuse.min.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform">
+ <script>
+ if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("native-css").disabled = false;
+ document.body.classList.remove("crossplatform");
+ }
+ </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>
+ </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;">
+ <?= $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 ?>
+ </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>
+ </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>
+
+ <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 favoriteSong(id) {
+ document.getElementById("btn-favorite-" + id + "-icon").src = "/assets/icons/favorite-on.svg";
+ 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).onclick = () => {
+ favoriteSong(id);
+ }
+ await fetch("/api/removeFavorite.php?i=" + id);
+
+ <?php if (isset($favoritesList)): ?>
+ location.reload();
+ <?php endif; ?>
+
+ window.parent.redownloadFavorites();
+ }
+
+ 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";
+ }
+ }
+ </script>
+
+ <br><br>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/app/lyrics.php b/app/lyrics.php
new file mode 100644
index 0000000..fbdca1d
--- /dev/null
+++ b/app/lyrics.php
@@ -0,0 +1,158 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>lyrics</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/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+ <style>
+ .synced-lyrics-item {
+ opacity: .25;
+ font-size: 4vw;
+ min-font-size: 22px;
+ margin-bottom: 10px;
+ transition: opacity 200ms;
+ }
+
+ .synced-lyrics-item.active {
+ opacity: 1;
+ }
+
+ ::-webkit-scrollbar {
+ display: none;
+ }
+
+ #lyrics-synced {
+ transition-property: top;
+ transition-duration: 500ms;
+ transition-timing-function: cubic-bezier(0.54, 0, 0.23, 0.79);
+ }
+ </style>
+</head>
+<body class="crossplatform" style="background-color: transparent !important;">
+ <script>
+ if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("native-css").disabled = false;
+ document.body.classList.remove("crossplatform");
+ }
+ </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;">
+ Lyrics will appear here when you start playing a song. If supported, they will also scroll as the song plays.
+ </div>
+
+ <div id="not-available" style="display: none; position: fixed; inset: 16px; align-items: center; justify-content: center; opacity: .5; text-align: center;">
+ No lyrics are available for this song. Try again later.
+ </div>
+
+ <div id="loading" style="display: none; position: fixed; inset: 16px; align-items: center; justify-content: center; opacity: .5; text-align: center;">
+ Loading lyrics...
+ </div>
+
+ <div id="lyrics-unsynced" style="display: none; position: fixed; inset: 16px; overflow: auto;"></div>
+ <div id="lyrics-synced" style="display: none; position: fixed; left: 16px; right: 16px; top: 0; bottom: 0; z-index: 5;"></div>
+ <div id="lyrics-synced-fade" style="display: none; position: fixed; inset: 0; z-index: 10; background-image: linear-gradient(180deg, rgba(255,0,0,0) 25%, rgba(255,255,255,1) 100%);"></div>
+ </div>
+
+ <script>
+ let lastID = null;
+ let lastTimestamp = null;
+ window.lyrics = {};
+
+ setInterval(async () => {
+ if (window.parent.currentSongID !== lastID) {
+ lastID = window.parent.currentSongID;
+
+ if (lastID === null) {
+ document.getElementById("lyrics-synced").style.display = "none";
+ document.getElementById("lyrics-synced-fade").style.display = "none";
+ document.getElementById("lyrics-unsynced").style.display = "none";
+ document.getElementById("not-available").style.display = "none";
+ document.getElementById("loading").style.display = "none";
+ document.getElementById("not-playing").style.display = "flex";
+ } else {
+ document.getElementById("lyrics-synced").style.display = "none";
+ document.getElementById("lyrics-synced-fade").style.display = "none";
+ document.getElementById("lyrics-unsynced").style.display = "none";
+ document.getElementById("not-available").style.display = "none";
+ document.getElementById("loading").style.display = "flex";
+ document.getElementById("not-playing").style.display = "none";
+
+ if (!window.lyrics[lastID]) {
+ try {
+ window.lyrics[lastID] = await (await fetch("/api/lyrics.php?id=" + lastID)).json()
+ } catch (e) {
+ window.lyrics[lastID] = {
+ synced: false,
+ payload: null
+ }
+ }
+
+ if (window.lyrics[lastID] && window.lyrics[lastID].payload) {
+ if (window.lyrics[lastID].synced) {
+ document.getElementById("lyrics-synced").style.display = "";
+ document.getElementById("lyrics-synced-fade").style.display = "";
+ document.getElementById("lyrics-unsynced").style.display = "none";
+ document.getElementById("not-available").style.display = "none";
+ document.getElementById("loading").style.display = "none";
+ document.getElementById("not-playing").style.display = "none";
+
+ 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>";
+ } else {
+ document.getElementById("lyrics-synced").style.display = "none";
+ document.getElementById("lyrics-synced-fade").style.display = "none";
+ document.getElementById("lyrics-unsynced").style.display = "";
+ document.getElementById("not-available").style.display = "none";
+ document.getElementById("loading").style.display = "none";
+ document.getElementById("not-playing").style.display = "none";
+
+ document.getElementById("lyrics-unsynced").innerText = window.lyrics[lastID].payload.replaceAll("\n\n\n", "\n");
+ }
+ } else {
+ document.getElementById("lyrics-synced").style.display = "none";
+ document.getElementById("lyrics-synced-fade").style.display = "none";
+ document.getElementById("lyrics-unsynced").style.display = "none";
+ document.getElementById("not-available").style.display = "flex";
+ document.getElementById("loading").style.display = "none";
+ document.getElementById("not-playing").style.display = "none";
+ }
+ }
+ }
+ }
+
+ if (window.lyrics[lastID] && window.lyrics[lastID].synced) {
+ document.getElementById("lyrics-synced").style.display = "";
+ document.getElementById("lyrics-synced-fade").style.display = "";
+ document.getElementById("lyrics-unsynced").style.display = "none";
+ document.getElementById("not-available").style.display = "none";
+ document.getElementById("loading").style.display = "none";
+ document.getElementById("not-playing").style.display = "none";
+
+ for (let item of [...window.lyrics[lastID].payload].reverse()) {
+ if (parseInt(item.startTimeMs) / 1000 <= window.parent.document.getElementById("player").contentDocument.getElementById("player-audio").currentTime) {
+ if (item.startTimeMs !== lastTimestamp) {
+ Array.from(document.getElementsByClassName("synced-lyrics-item")).map(i => i.classList.remove("active"));
+ document.getElementById("lyrics-synced").style.top = "-" + (document.getElementById("synced-lyrics-" + item.startTimeMs).offsetTop - 33) + "px";
+ lastTimestamp = item.startTimeMs;
+ document.getElementById("synced-lyrics-" + item.startTimeMs).classList.add("active");
+ }
+
+ break;
+ }
+ }
+ }
+ }, 100);
+ </script>
+</body>
+</html> \ No newline at end of file
diff --git a/app/navigation.php b/app/navigation.php
new file mode 100644
index 0000000..f07c38f
--- /dev/null
+++ b/app/navigation.php
@@ -0,0 +1,66 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFILE ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>navigation</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">
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+ <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/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform navigation-body">
+ <script>
+ if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("native-css").disabled = false;
+ document.body.classList.remove("crossplatform");
+ }
+ </script>
+ <div class="navigation-container-inner container" style="margin-left: 20px;">
+ <div id="navigation-left">
+ <div id="navigation-gradient" style="background-image: radial-gradient(circle, rgba(122,195,245,.25) 0%, rgba(255,255,255,0) 75%); top: -256px; left: 0; right: 0; height: 512px; position: fixed; z-index: -1;"></div>
+ <div id="navigation-top" style="-webkit-app-region: drag; position: fixed; top: 0; right: 0; left: 0; height: 128px;"></div>
+ <br><br><br>
+ <h3 style="margin-left: 10px;">
+ <img src="/assets/logo-display.png" alt="" style="width: 32px; height: 32px; vertical-align: middle;">
+ <span style="vertical-align: middle;" class="navigation-desktop">&nbsp;Mist</span>
+ </h3>
+ <br>
+ </div>
+
+ <div id="navigation-container">
+ <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');">
+ <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');">
+ <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');">
+ <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();">
+ <img class="icon" alt="" src="/assets/icons/lyrics.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Lyrics</span>
+ </div>
+ <div id="settings" class="navigation-item" onclick="window.parent.openUI('settings');">
+ <img class="icon" alt="" src="/assets/icons/settings.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Settings</span>
+ </div>
+ </div>
+
+ <div id="navigation-version">
+ <a style="position: fixed; width: 100%; bottom: 60px; color: inherit; text-decoration: inherit; display: block;" id="account" class="navigation-item" href="https://account.equestria.dev/hub/users/<?= $_PROFILE["id"] ?>" target="_blank">
+ <img alt="" src="https://account.equestria.dev/hub/api/rest/avatar/<?= $_PROFILE["id"] ?>?dpr=2&size=32" style="filter: none !important; border-radius: 999px; vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop"><?= $_PROFILE["name"] ?></span>
+ </a>
+ </div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/app/player-mobile.php b/app/player-mobile.php
new file mode 100644
index 0000000..cab8ada
--- /dev/null
+++ b/app/player-mobile.php
@@ -0,0 +1,79 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>player-mobile</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
+ <link href="/assets/dark.css" rel="stylesheet">
+ <link href="/assets/styles.css" rel="stylesheet">
+ <script src="/assets/shortcuts.js"></script>
+ <style>
+ .player-btn {
+ filter: invert(1);
+ opacity: .75;
+ }
+ </style>
+</head>
+<body>
+ <div id="act-1" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; top: 0; left: 0; right: 0; height: 20px;"></div>
+ <div id="act-2" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; bottom: 0; left: 0; right: 0; height: 20px;"></div>
+ <div id="act-2" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; bottom: 0; left: 0; top: 0; width: 20px;"></div>
+ <div id="act-2" onclick="window.parent.hideMobilePlayer();" style="position: fixed; z-index: 9999999; bottom: 0; right: 0; top: 0; width: 20px;"></div>
+
+ <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="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: 0; left: 0; right: 0; height: 64px; z-index: 9999;">
+ <div class="container" style="display: grid; grid-template-columns: 1fr 1.5fr 1fr;">
+ <div id="buttons" style="height: 48px;position: fixed;width: max-content;left: 0;right: 0;margin: 8px auto;bottom: 30px;">
+ <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">
+ <img alt="" src="/assets/icons/shuffle-off.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-shuffle-icon">
+ </span>
+ <span onclick="window.parent.previous();" class="player-btn disabled" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-previous">
+ <img alt="" src="/assets/icons/previous.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-previous-icon">
+ </span>
+ <span onclick="window.parent.playPause();" class="player-btn" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-play">
+ <img alt="" src="/assets/icons/play.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-play-icon">
+ </span>
+ <span onclick="window.parent.next();" class="player-btn disabled" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-next">
+ <img alt="" src="/assets/icons/next.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-next-icon">
+ </span>
+ <span onclick="window.parent.toggleRepeat();" class="player-btn" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-repeat">
+ <img alt="" src="/assets/icons/repeat-off.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-repeat-icon">
+ </span>
+ </div>
+ <div style="position: fixed;top: 30px;left: 30px;right: 30px;">
+ <img alt="" id="album-art" style="border-radius: 5px; background-color: rgba(0, 0, 0, .1); height: 48px !important; width: 48px !important;">
+ <div id="info-grid" style="display: grid;grid-template-rows: 22px 22px;position: fixed;top: 30px;left: 88px;">
+ <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: left;justify-content: left; opacity: .75;"><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: left;justify-content: left;opacity: .35;"><span id="artist">Artist</span><span style="display: none;">&nbsp;—&nbsp;<span id="album">Album</span></span></div>
+ <div id="info-grid-time" style="font-size: 9px;opacity: .5;margin-left: 2px;margin-right: 2px;position: fixed;bottom: 125px;left: 30px;right: 30px;">
+ <span id="elapsed-time">0:00</span>
+ <span id="remaining-time" style="float: right;">-0:00</span>
+ </div>
+ <div style="background-color: rgba(255, 255, 255, .05);position: fixed;bottom: 150px;left: 30px;right: 30px;border-radius: 999px;" id="seekbar-container">
+ <div id="seekbar" style="pointer-events: none;background-color: rgba(255, 255, 255, .25); width: 0; height: 8px;border-radius: 999px;"></div>
+ </div>
+ </div>
+ </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: 120px;">
+ <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>
+ </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: 120px;">
+ <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>
+ </span>
+ </div>
+ </div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/app/player.php b/app/player.php
new file mode 100644
index 0000000..e2334ce
--- /dev/null
+++ b/app/player.php
@@ -0,0 +1,76 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>player</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
+ <link href="/assets/dark.css" rel="stylesheet">
+ <link href="/assets/styles.css" rel="stylesheet">
+ <script src="/assets/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform">
+ <script>
+ if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("native-css").disabled = false;
+ document.body.classList.remove("crossplatform");
+ }
+ </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>
+ <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">
+ <img class="icon" alt="" src="/assets/icons/shuffle-off.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-shuffle-icon">
+ </span>
+ <span onclick="window.parent.previous();" class="player-btn disabled" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-previous">
+ <img class="icon" alt="" src="/assets/icons/previous.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-previous-icon">
+ </span>
+ <span onclick="window.parent.playPause();" class="player-btn" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-play">
+ <img class="icon" alt="" src="/assets/icons/play.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-play-icon">
+ </span>
+ <span onclick="window.parent.next();" class="player-btn disabled" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-next">
+ <img class="icon" alt="" src="/assets/icons/next.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-next-icon">
+ </span>
+ <span onclick="window.parent.toggleRepeat();" class="player-btn" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-repeat">
+ <img class="icon" alt="" src="/assets/icons/repeat-off.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-repeat-icon">
+ </span>
+ </div>
+ <div>
+ <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-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-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>
+ </div>
+ <div style="background-color: rgba(0, 0, 0, .05);" id="seekbar-container">
+ <div id="seekbar" style="pointer-events: none; background-color: rgba(0, 0, 0, .25); width: 0; height: 8px;"></div>
+ </div>
+ </div>
+ </div>
+ <div id="cover" style="display: grid; 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="" src="/assets/nothing.svg" id="album-art" style="background-color: rgba(0, 0, 0, .1); height: 64px; width: 64px;">
+ <div style="display: flex; align-items: center; justify-content: center;">
+ <img class="icon" src="/assets/logo-transparent.svg" style="filter: grayscale(1) invert(1); width: 32px; height: 32px;" alt="">
+ </div>
+ </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>
+</body>
+</html> \ No newline at end of file
diff --git a/app/settings.php b/app/settings.php
new file mode 100644
index 0000000..4c2a287
--- /dev/null
+++ b/app/settings.php
@@ -0,0 +1,68 @@
+<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <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>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">
+ <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/shortcuts.js"></script>
+ <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
+</head>
+<body class="crossplatform">
+ <script>
+ if (navigator.userAgent.includes("MistNative/darwin") || navigator.userAgent.includes("MistNative/win32")) {
+ document.getElementById("native-css").disabled = false;
+ document.body.classList.remove("crossplatform");
+ }
+ </script>
+ <div class="container">
+ <br>
+ <h2 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>
+ <script>
+ if (localStorage.getItem("data-saving") === "true") document.getElementById("data-saving").checked = true;
+ function saveDS() {
+ localStorage.setItem("data-saving", document.getElementById("data-saving").checked ? "true" : "false");
+ 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">
+ <label class="form-check-label" for="desktop-notification">Display notification when song changes</label>
+ <div class="text-muted small">If this is enabled, a desktop notification will be shown when the song being played changes, containing information about the new song. This requires having notifications enabled in your system settings.</div>
+ </div>
+ <script>
+ if (localStorage.getItem("desktop-notification") === "true") document.getElementById("desktop-notification").checked = true;
+ function saveDN() {
+ localStorage.setItem("desktop-notification", document.getElementById("desktop-notification").checked ? "true" : "false");
+ }
+ </script>
+ <?php endif; ?>
+
+ <hr>
+ <?php if (str_contains($_SERVER['HTTP_USER_AGENT'], "MistNative/")): ?>
+ <a onclick="window.parent.MistNative.about();" href="#">About Mist</a>
+ <?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 <?= file_get_contents("/opt/spotify/build.txt") ?>) · © <?= date('Y') ?> Equestria.dev</span>
+ </div>
+ <?php endif; ?>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/app/songs.php b/app/songs.php
new file mode 100644
index 0000000..086df66
--- /dev/null
+++ b/app/songs.php
@@ -0,0 +1,4 @@
+<?php
+
+unset($_GET["a"]);
+require_once "./listing.php"; \ No newline at end of file