summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2023-10-31 17:04:34 +0100
committerRaindropsSys <raindrops@equestria.dev>2023-10-31 17:04:34 +0100
commite61e581a2b66b0444db01d884465ea913929e343 (patch)
treeb49eaedb3681c4b26637acdd375cda4c3d37b07c
parent41c51b8bdb9c8e9fa4a7d56f260d594739d4107e (diff)
downloadmist-e61e581a2b66b0444db01d884465ea913929e343.tar.gz
mist-e61e581a2b66b0444db01d884465ea913929e343.tar.bz2
mist-e61e581a2b66b0444db01d884465ea913929e343.zip
Updated 27 files, added 12 files and deleted 3 files (automated)
-rw-r--r--.DS_Storebin18436 -> 18436 bytes
-rw-r--r--albumart.php4
-rw-r--r--android/.idea/misc.xml1
-rw-r--r--android/app/release/app-release.apkbin22996047 -> 22997059 bytes
-rw-r--r--android/app/release/app-release.apk.zipbin8300789 -> 0 bytes
-rw-r--r--android/app/src/main/java/dev/equestria/mist/MainActivity.kt41
-rw-r--r--api/addHistory.php5
-rw-r--r--api/savePrivacy.php13
-rw-r--r--api/saveProfile.php40
-rw-r--r--app/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--app/index.php52
-rw-r--r--app/ui/albums.php2
-rw-r--r--app/ui/explore.php54
-rw-r--r--app/ui/favorites.php29
-rw-r--r--app/ui/home.php137
-rw-r--r--app/ui/info.php1
-rw-r--r--app/ui/library.php2
-rw-r--r--app/ui/listing.php56
-rw-r--r--app/ui/logout.php6
-rw-r--r--app/ui/navigation.php4
-rw-r--r--app/ui/player.php63
-rw-r--r--app/ui/search.php4
-rw-r--r--app/ui/settings.php226
-rw-r--r--app/ui/update.php2
-rw-r--r--app/ui/welcome-dp.php59
-rw-r--r--app/ui/welcome.php2
-rw-r--r--assets/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--assets/dark.css8
-rw-r--r--assets/icons/home.svg1
-rw-r--r--assets/icons/search.svg1
-rw-r--r--assets/icons/volume-down.svg1
-rw-r--r--assets/icons/volume-up.svg1
-rw-r--r--assets/styles.css67
-rw-r--r--icons/adult.svg1
-rw-r--r--icons/logo-transparent.svg61
-rw-r--r--includes/Parsedown.php1994
-rw-r--r--includes/session.php18
-rw-r--r--oauth/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--oauth/callback-native/index.php3
-rw-r--r--oauth/callback/index.php3
-rw-r--r--profile/index.php418
-rw-r--r--version2
42 files changed, 3126 insertions, 256 deletions
diff --git a/.DS_Store b/.DS_Store
index cc1b887..9512492 100644
--- a/.DS_Store
+++ b/.DS_Store
Binary files differ
diff --git a/albumart.php b/albumart.php
index c2551fd..0a7e445 100644
--- a/albumart.php
+++ b/albumart.php
@@ -1,11 +1,13 @@
<?php
$songs = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/assets/content/songs.json"), true);
+$albums = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/assets/content/albums.json"), true);
-if (!isset($_GET["i"]) || !isset($songs[$_GET["i"]])) {
+if (!isset($_GET["i"]) || (!isset($songs[$_GET["i"]]) && !isset($albums[$_GET["i"]]))) {
die();
}
+header("Cache-Control: public, max-age=604800, immutable");
header("Content-Type: image/jpeg");
header("Content-Length: " . filesize($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".jpg"));
readfile($_SERVER['DOCUMENT_ROOT'] . "/assets/content/" . $_GET["i"] . ".jpg"); \ No newline at end of file
diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml
index 0ad17cb..8978d23 100644
--- a/android/.idea/misc.xml
+++ b/android/.idea/misc.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
diff --git a/android/app/release/app-release.apk b/android/app/release/app-release.apk
index d1478d9..5dc2a7d 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
deleted file mode 100644
index 089aac4..0000000
--- a/android/app/release/app-release.apk.zip
+++ /dev/null
Binary files differ
diff --git a/android/app/src/main/java/dev/equestria/mist/MainActivity.kt b/android/app/src/main/java/dev/equestria/mist/MainActivity.kt
index 939feaa..930d309 100644
--- a/android/app/src/main/java/dev/equestria/mist/MainActivity.kt
+++ b/android/app/src/main/java/dev/equestria/mist/MainActivity.kt
@@ -7,14 +7,13 @@ import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
-import android.media.MediaMetadata
+import android.net.Uri
import android.os.Build
import android.os.Bundle
-import android.support.v4.media.MediaMetadataCompat
-import android.support.v4.media.session.MediaSessionCompat
-import android.support.v4.media.session.PlaybackStateCompat
+import android.os.Message
import android.util.Log
import android.view.ViewGroup
+import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
@@ -23,45 +22,30 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.WindowInsetsControllerCompat
-import androidx.media.app.NotificationCompat as MediaNotificationCompat
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.core.content.ContextCompat
import com.android.volley.Request
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.equestria.mist.ui.theme.MistTheme
+
class MainActivity : ComponentActivity() {
private lateinit var intent: Intent
@@ -196,6 +180,23 @@ fun WebViewContainer(activity: MainActivity, intent: Intent) {
settings.userAgentString += " MistAndroid/" + BuildConfig.VERSION_NAME
settings.cacheMode = WebSettings.LOAD_NO_CACHE
+ settings.setSupportMultipleWindows(true)
+ webChromeClient = object : WebChromeClient() {
+ override fun onCreateWindow(
+ view: WebView,
+ dialog: Boolean,
+ userGesture: Boolean,
+ resultMsg: Message
+ ): Boolean {
+ val result = view.hitTestResult
+ val data = result.extra
+ val context = view.context
+ val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(data))
+ context.startActivity(browserIntent)
+ return false
+ }
+ }
+
addJavascriptInterface(JavaScriptExtensions(activity, activity.window, this, intent), "MistAndroid")
loadUrl(mUrl)
}
diff --git a/api/addHistory.php b/api/addHistory.php
index 117fc06..e5dea25 100644
--- a/api/addHistory.php
+++ b/api/addHistory.php
@@ -5,6 +5,9 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php";
global $songs; global $_PROFILE; global $history;
if (!isset($_GET["i"]) || !isset($songs[$_GET["i"]])) return;
-$history[] = $_GET["i"];
+$history[] = [
+ "item" => $_GET["i"],
+ "date" => date('c')
+];
file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-history.json", json_encode($history)); \ No newline at end of file
diff --git a/api/savePrivacy.php b/api/savePrivacy.php
new file mode 100644
index 0000000..b18f0f2
--- /dev/null
+++ b/api/savePrivacy.php
@@ -0,0 +1,13 @@
+<?php
+
+header("X-Frame-Options: SAMEORIGIN");
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php";
+global $songs; global $_PROFILE; global $favorites; global $privacy;
+
+foreach ($privacy as $item => $_) {
+ if (isset($_GET[$item]) && ($_GET[$item] === "0" || $_GET[$item] === "1" || $_GET[$item] === "2")) {
+ $privacy[$item] = (int)$_GET[$item];
+ }
+}
+
+file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-privacy.json", json_encode($privacy)); \ No newline at end of file
diff --git a/api/saveProfile.php b/api/saveProfile.php
new file mode 100644
index 0000000..eee3e9c
--- /dev/null
+++ b/api/saveProfile.php
@@ -0,0 +1,40 @@
+<?php
+
+header("X-Frame-Options: SAMEORIGIN");
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $profile; global $_PROFILE;
+
+if (isset($_POST["nsfw"])) {
+ if ($_POST["nsfw"] === "false") {
+ $profile["nsfw"] = false;
+ } else {
+ $profile["nsfw"] = true;
+ }
+}
+
+if (isset($_POST["description"])) {
+ $profile["description"] = substr(strip_tags($_POST["description"]), 0, 500);
+}
+
+if (isset($_POST["url"])) {
+ $derpibooruID = preg_replace("/^http(s|):\/\/(www\.|)derpibooru\.org\/images\/(\d+).*$/m", "$3", $_POST["url"]);
+
+ if ($derpibooruID === $_POST["url"]) {
+ $profile["banner"] = $profile["banner_orig"] = substr(strip_tags($_POST["url"]), 0, 120);
+ } else {
+ $data = json_decode(file_get_contents("https://derpibooru.org/api/v1/json/images/" . $derpibooruID, false, stream_context_create([
+ "http" => [
+ "method" => "GET",
+ "header" => "User-Agent: Mozilla/5.0 (+MistProfile/1.0; raindrops@equestria.dev)\r\n"
+ ]
+ ])), true);
+
+ if (isset($data) && json_last_error() === JSON_ERROR_NONE && isset($data["image"]) && isset($data["image"]["view_url"])) {
+ $profile["banner"] = $data["image"]["view_url"];
+ $profile["banner_orig"] = substr(strip_tags($_POST["url"]), 0, 120);
+ } else {
+ $profile["banner"] = $profile["banner_orig"] = substr(strip_tags($_POST["url"]), 0, 120);
+ }
+ }
+}
+
+file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-profileSettings.json", json_encode($profile)); \ No newline at end of file
diff --git a/app/.DS_Store b/app/.DS_Store
index 3c6d6b1..7889376 100644
--- a/app/.DS_Store
+++ b/app/.DS_Store
Binary files differ
diff --git a/app/index.php b/app/index.php
index 6989866..601c974 100644
--- a/app/index.php
+++ b/app/index.php
@@ -37,11 +37,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
<script src="/assets/js/common.js"></script>
<script>
if (location.hash.trim() === "") {
- if (window.innerWidth < 863) {
- location.hash = "#/library";
- } else {
- location.hash = "#/albums";
- }
+ location.hash = "#/home";
}
if (window.MistNative) {
@@ -77,17 +73,13 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
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") ?>") {
+ if (localStorage.getItem("lastUpdate") !== "<?= trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")) ?>") {
openModal("What's new in Mist?", "update.php", true);
}
}
- <?php endif; ?>
}
window.onerror = (_1, _2, _3, _4, err) => {
@@ -142,11 +134,15 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
document.getElementById("lyrics-page").style.display = "none";
document.getElementById("ui").style.display = "";
setSrcIfDifferent("ui/listing.php?a=" + location.hash.split("/")[2]);
+ } else if (name === "favorites" && location.hash.split("/")[2]) {
+ document.getElementById("lyrics-page").style.display = "none";
+ document.getElementById("ui").style.display = "";
+ setSrcIfDifferent("ui/favorites.php?u=" + 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";
+ name = "home";
} else {
document.getElementById("lyrics-page").style.display = "none";
document.getElementById("ui").style.display = "";
@@ -154,6 +150,18 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
}
}
+ window.setPlayerVolume = () => {
+ let volume = parseFloat(document.getElementById("player").contentDocument.getElementById("volume").value);
+ localStorage.setItem("volume", volume);
+
+ document.getElementById("player").contentDocument.getElementById("player-audio").volume = volume / 100;
+ document.getElementById("player").contentDocument.getElementById("player-audio-stella-side1").volume = volume / 100;
+ document.getElementById("player").contentDocument.getElementById("player-audio-stella-side2").volume = volume / 100;
+ document.getElementById("player").contentDocument.getElementById("player-audio-stella-side3").volume = volume / 100;
+ document.getElementById("player").contentDocument.getElementById("player-audio-stella-side4").volume = volume / 100;
+ document.getElementById("player").contentDocument.getElementById("player-audio-stella-side5").volume = volume / 100;
+ }
+
loadHash();
document.getElementById("ui").onload = () => {
@@ -162,10 +170,24 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFI
}
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");
+ class MistNavigationError extends Error {
+ constructor(message, stack) {
+ super(message);
+ this.name = "MistNavigationError";
+ this.message = message;
+ this.stack = this.stack + "\n\n" + stack;
+ }
+ }
+
+ try {
+ if (!name || typeof name !== "string") name = window.name;
+ if (name === "lyrics") showLyrics();
+ if (name === "search") name = "home";
+ Array.from(document.getElementById("navigation").contentDocument.getElementsByClassName("navigation-item")).map(i => i.classList.remove("active"));
+ document.getElementById("navigation").contentDocument.getElementById(name).classList.add("active");
+ } catch (e) {
+ throw new MistNavigationError("Error while navigating to " + name + ": " + e.message, e.stack);
+ }
}
let loadedPlayers = 0;
diff --git a/app/ui/albums.php b/app/ui/albums.php
index 1ee5822..29bd945 100644
--- a/app/ui/albums.php
+++ b/app/ui/albums.php
@@ -66,7 +66,7 @@
<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 class="btn btn-primary" onclick="window.parent.openUI('home');">Browse Mist</div>
</div>
</div>
<?php endif; ?>
diff --git a/app/ui/explore.php b/app/ui/explore.php
deleted file mode 100644
index c93beff..0000000
--- a/app/ui/explore.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?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/#/explore";
- }
- </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>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/js/shortcuts.js"></script>
- <link id="native-css" href="/assets/native.css" rel="stylesheet" disabled>
-</head>
-<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>
-
- <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>
- </div>
- </form>
-
- <hr style="width: calc(100% - 20px); margin-left: 10px;">
-
- <p style="margin-left: 10px;">To search for content on Mist, start by typing the name of a song, artist or album. Any corresponding results will show up. Should anything you need be missing, you can contact your administrator to ask for more content to be added.</p>
- </div>
-
- <br><br>
-</body>
-</html> \ No newline at end of file
diff --git a/app/ui/favorites.php b/app/ui/favorites.php
index f1ebe30..76821fd 100644
--- a/app/ui/favorites.php
+++ b/app/ui/favorites.php
@@ -2,13 +2,38 @@
header("X-Frame-Options: SAMEORIGIN");
require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php";
-global $songs; global $favorites;
+global $songs; global $favorites; global $_PROFILE;
$hasAlbum = false;
$favoritesList = true;
$list = [];
-foreach ($favorites as $id) {
+if (!isset($_GET["u"])) {
+ header("Location: favorites.php?u=" . $_PROFILE["id"]);
+ die();
+}
+
+$correctFavorites = $favorites;
+
+if (preg_match("/[^a-f0-9-]/m", $_GET["u"]) == 0 && $_GET["u"] !== $_PROFILE["id"]) {
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_GET["u"] . "-privacy.json")) {
+ $userPrivacy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_GET["u"] . "-privacy.json"), true);
+ if ($userPrivacy["listen"] >= 1) {
+ $userId = $_GET["u"];
+ $correctFavorites = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_GET["u"] . "-favorites.json"), true);
+ } else {
+ header("Location: favorites.php?u=" . $_PROFILE["id"]);
+ die();
+ }
+ } else {
+ header("Location: favorites.php?u=" . $_PROFILE["id"]);
+ die();
+ }
+} else {
+ $userId = $_PROFILE["id"];
+}
+
+foreach ($correctFavorites as $id) {
if (isset($songs[$id])) $list[$id] = $songs[$id];
}
diff --git a/app/ui/home.php b/app/ui/home.php
new file mode 100644
index 0000000..e51c790
--- /dev/null
+++ b/app/ui/home.php
@@ -0,0 +1,137 @@
+<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $history; global $profile; global $_PROFILE; global $songs; global $albums; ?>
+<!doctype html>
+<html lang="en">
+<head>
+ <script>
+ if (typeof window.parent.openModal === "undefined") {
+ location.href = "/app/#/home";
+ }
+ </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>Home</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>
+ <style>
+ #play-item:hover {
+ background-color: rgba(0, 0, 0, .05);
+ }
+
+ #play-item:active {
+ background-color: rgba(0, 0, 0, .1);
+ }
+ </style>
+</head>
+<body class="crossplatform">
+ <script>
+ window.parent.location.hash = "#/home";
+ </script>
+ <script src="/assets/js/common.js"></script>
+ <div class="container">
+ <div style="margin-left: 10px;">
+ <?php if (trim($profile["banner"]) !== ""): ?>
+ <div id="banner" style="background-size: cover; background-position: center; background-image: url(&quot;<?= str_replace('"', '', $profile["banner"]) ?>&quot;); background-color: #eee; height: 256px; margin-top: 20px; border-radius: 20px;">
+ <div style="background-color: rgba(255, 255, 255, .25); height: 100%; border-radius: 20px;"></div>
+ <?php else: ?>
+ <div id="banner" style="background-size: cover; background-position: center; background-image: url('https://account.equestria.dev/hub/api/rest/avatar/<?= $_PROFILE["id"] ?>?dpr=2&size=32'); background-color: #eee; height: 256px; margin-top: 20px; border-radius: 20px;">
+ <div style="background-color: rgba(255, 255, 255, .25); height: 100%; backdrop-filter: blur(100px); -webkit-backdrop-filter: blur(50px); border-radius: 20px;"></div>
+ <?php endif; ?>
+ </div>
+ <br>
+
+ <div style="display: grid; grid-template-columns: 64px 1fr max-content; grid-gap: 15px;">
+ <img alt="" src="https://account.equestria.dev/hub/api/rest/avatar/<?= $_PROFILE["id"] ?>?dpr=2&size=128" style="filter: none !important; border-radius: 999px; vertical-align: middle; width: 64px;">
+ <div style="display: flex; align-items: center;">
+ <div id="name">
+ <h4>Welcome <?= $_PROFILE["name"] ?></h4>
+ </div>
+
+ <div style="width: 100%; display: none;" id="search">
+ <div style="width: 100%;">
+ <input name="q" type="text" id="query" class="form-control" placeholder="Search on Mist">
+ </div>
+ </div>
+ </div>
+ <div style="display: flex; align-items: center;">
+ <span onclick="toggleSearch();" class="player-btn" style="border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; height: 48px; width: 48px;" id="btn-search">
+ <img class="icon" alt="" src="/assets/icons/search.svg" style="pointer-events: none; width: 32px; height: 32px;" id="btn-search-icon">
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group" style="margin-top: 20px; display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 10px;">
+ <?php usort($history, function ($a, $b) {
+ return strtotime($b["date"]) - strtotime($a["date"]);
+ }); foreach (array_slice(array_values(array_unique(array_map(function ($i) { return $i["item"]; }, array_values($history)))), 0, 12) as $item): $song = $songs[$item]; ?>
+ <div onclick="window.parent.playSong('<?= $item ?>')" id="play-item" class="list-group-item" style="border-radius: 5px !important; border: none !important; display: grid; grid-template-columns: 64px 1fr; white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;">
+ <div style="display: flex; align-items: center;">
+ <img alt="" src="/assets/content/<?= $item ?>.jpg" style="width: 48px; height: 48px; background-color: #eee; border-radius: 3px;">
+ </div>
+ <div style="display: flex; align-items: center;">
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;">
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $song["title"] ?></div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;" class="text-muted"><?= $song["artist"] ?></div>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+
+ <div id="album-grid" style="display: grid; grid-template-columns: repeat(5, 1fr); margin-top: 20px;">
+ <?php global $albums;
+
+ $albums = array_filter($albums, function ($i) {
+ global $library;
+ return in_array($i, $library);
+ }, ARRAY_FILTER_USE_KEY);
+
+ $albums = array_reverse($albums);
+
+ 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>
+
+ <script>
+ document.getElementById("query").onkeydown = (e) => {
+ if (e.code === "Enter") {
+ toggleSearch();
+ }
+ }
+
+ function toggleSearch() {
+ if (document.getElementById("search").style.display === "none") {
+ document.getElementById("search").style.display = "";
+ document.getElementById("name").style.display = "none";
+ document.getElementById("query").focus();
+ } else {
+ document.getElementById("query").blur();
+
+ if (document.getElementById("query").value.trim() === "") {
+ document.getElementById("search").style.display = "none";
+ document.getElementById("name").style.display = "";
+ } else {
+ window.parent.location.hash = '#/search/' + encodeURIComponent(document.getElementById('query').value);
+ location.href = "ui/search.php?q=" + encodeURIComponent(document.getElementById("query").value);
+ }
+ }
+ }
+ </script>
+
+ <br><br>
+</body>
+</html> \ No newline at end of file
diff --git a/app/ui/info.php b/app/ui/info.php
index d21e431..d857509 100644
--- a/app/ui/info.php
+++ b/app/ui/info.php
@@ -201,6 +201,7 @@ function getChannelConfiguration($c) {
<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>
+ <a style="float: right;" class="btn btn-outline-secondary" onclick="window.parent._modal.hide();">Close</a>
</div>
<script>
diff --git a/app/ui/library.php b/app/ui/library.php
index c6b5658..4736a11 100644
--- a/app/ui/library.php
+++ b/app/ui/library.php
@@ -23,7 +23,7 @@
</head>
<body class="crossplatform">
<script>
- window.parent.location.hash = "#/search/<?= rawurlencode($_GET["q"]) ?>";
+ window.parent.location.hash = "#/library";
</script>
<script src="/assets/js/common.js"></script>
<div class="container">
diff --git a/app/ui/listing.php b/app/ui/listing.php
index d32525b..510fa58 100644
--- a/app/ui/listing.php
+++ b/app/ui/listing.php
@@ -87,15 +87,30 @@ if (!$presetList) {
<script>
window.parent.location.hash = "#/albums/<?= $_GET["a"] ?>";
</script>
+ <?php endif; if (isset($favoritesList)): global $userId; ?>
+ <script>
+ window.parent.location.hash = "#/favorites/<?= $userId ?>";
+ </script>
<?php endif; ?>
<div class="container">
<br>
- <?php if (isset($favoritesList) && !$hasAlbum): ?>
+ <?php if (isset($favoritesList) && !$hasAlbum): global $userId; ?>
<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>
+ <h2 style="opacity: .5;">
+ <select onchange="changeView();" id="favorites-user-select" class="form-select" style="width: max-content;font-size: inherit;margin: -0.375rem 0 -0.375rem -0.75rem;">
+ <option <?= $userId === $_PROFILE["id"] ? "selected" : "" ?> value="<?= $_PROFILE["id"] ?>"><?= $_PROFILE["name"] ?></option>
+ <?php foreach (scandir($_SERVER['DOCUMENT_ROOT'] . "/includes/users") as $user):
+ if (str_ends_with($user, "-privacy.json")):
+ $userPrivacy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $user), true);
+ $userProfile = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . substr($user, 0, -13) . "-profile.json"), true);
+ if ($userPrivacy["listen"] >= 1 && $userProfile["id"] !== $_PROFILE["id"]): ?>
+ <option <?= $userId === $userProfile["id"] ? "selected" : "" ?> value="<?= $userProfile["id"] ?>"><?= $userProfile["name"] ?></option>
+ <?php endif; endif; endforeach; ?>
+ </select>
+ </h2>
<div style="opacity: .5;">
Click on the heart icon near a song to add it to this list.
</div>
@@ -153,7 +168,7 @@ if (!$presetList) {
<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 class="btn btn-primary" onclick="window.parent.openUI('home');">Browse Mist</div>
</div>
</div>
<?php endif; ?>
@@ -183,29 +198,16 @@ if (!$presetList) {
}
<?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
- }
- ]
- });
+ let items = Array.from(document.getElementsByClassName("track")).map(i => { return { title: i.getAttribute("data-item-track").toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " "), artist: i.getAttribute("data-item-artist").toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " "), id: i.id } });
function updateFilter() {
- let query = document.getElementById("filter").value.trim();
+ let query = document.getElementById("filter").value.trim().toLowerCase().replace(/[^a-z\d ]/mg, " ").replace(/ +/mg, " ");
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, " ")));
+ let results = items.filter(i => i.title.includes(query) || i.artist.includes(query)).slice(0, 50);
document.getElementById("search-results").innerHTML = "";
for (let result of results) {
@@ -216,8 +218,24 @@ if (!$presetList) {
document.getElementById("main-list").style.display = "flex";
}
}
+
+ function changeView() {
+ location.href = "favorites.php?u=" + document.getElementById("favorites-user-select").value;
+ }
</script>
+ <style>
+ #favorites-user-select {
+ background-color: transparent;
+ border-color: transparent;
+ }
+
+ #favorites-user-select:hover, #favorites-user-select:active, #favorites-user-select:focus {
+ background-color: var(--bs-body-bg);
+ border-color: var(--bs-border-color);
+ }
+ </style>
+
<br><br>
</body>
</html> \ No newline at end of file
diff --git a/app/ui/logout.php b/app/ui/logout.php
new file mode 100644
index 0000000..0ea605e
--- /dev/null
+++ b/app/ui/logout.php
@@ -0,0 +1,6 @@
+<?php header("X-Frame-Options: SAMEORIGIN");
+header("Set-Cookie: WAVY_SESSION_TOKEN=; SameSite=None; Path=/; Secure; HttpOnly; Expires=" . date("r", time() + (86400 * 730)));
+?>
+<script>
+ window.parent.location.href = "/app/";
+</script> \ No newline at end of file
diff --git a/app/ui/navigation.php b/app/ui/navigation.php
index dacaa94..0125418 100644
--- a/app/ui/navigation.php
+++ b/app/ui/navigation.php
@@ -36,8 +36,8 @@
</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 id="home" class="navigation-item" onclick="window.parent.openUI('home');">
+ <img class="icon" alt="" src="/assets/icons/home.svg" style="vertical-align: middle; width: 32px;"><span style="vertical-align: middle; margin-left: 5px;" class="navigation-desktop">Home</span>
</div>
<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>
diff --git a/app/ui/player.php b/app/ui/player.php
index 8043a5d..4c5298c 100644
--- a/app/ui/player.php
+++ b/app/ui/player.php
@@ -75,27 +75,29 @@
<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">
- <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 id="buttons" style="text-align: center; display: flex; align-items: center; justify-content: center;">
+ <div 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>
- <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>
+ <span class="player-badge-desktop" 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 class="player-badge-desktop" 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 class="player-badge-desktop" 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="z-index: 9; display: grid; grid-template-rows: 2px 22px 22px 12px 6px;">
@@ -118,13 +120,28 @@
</div>
</div>
</div>
- <div style="text-align: right; display: flex; align-items: center; justify-content: right;" id="badges">
-
+ <div style="text-align: center; display: flex; align-items: center; justify-content: center;" id="badges">
+ <img src="/assets/icons/volume-down.svg" alt="" class="icon" style="margin-right: 10px; display: inline-block;">
+ <input onchange="window.parent.setPlayerVolume();" oninput="window.parent.setPlayerVolume();" min="0" max="100" step="0.01" value="100" type="range" class="form-range" id="volume" style="width: 128px;">
+ <img src="/assets/icons/volume-up.svg" alt="" class="icon" style="margin-left: 10px; display: inline-block;">
</div>
</div>
</div>
<script>
+ if (localStorage.getItem("volume")) {
+ let vol = parseFloat(localStorage.getItem("volume"));
+
+ if (!isNaN(vol) && vol >= 0 && vol <= 100) {
+ document.getElementById("volume").value = vol;
+ window.parent.setPlayerVolume();
+ } else {
+ localStorage.setItem("volume", "100");
+ }
+ } else {
+ localStorage.setItem("volume", "100");
+ }
+
window.buildTooltips = () => {
console.log("Build tooltip");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
@@ -136,13 +153,13 @@
}
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);
+ 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, true);
}
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");
+ window.parent.redoNavigation("home");
}
function openAlbum() {
diff --git a/app/ui/search.php b/app/ui/search.php
index 0a1c3be..268ca5e 100644
--- a/app/ui/search.php
+++ b/app/ui/search.php
@@ -1,7 +1,7 @@
<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php";
if (!isset($_GET["q"]) || trim($_GET["q"]) === "" || trim(preg_replace("/ +/m", " ", preg_replace("/[^a-z\d ]/m", " ", strtolower($_GET["q"])))) === "") {
- header("Location: explore.php");
+ header("Location: home.php");
die();
}
@@ -11,7 +11,7 @@ if (!isset($_GET["q"]) || trim($_GET["q"]) === "" || trim(preg_replace("/ +/m",
<head>
<script>
if (typeof window.parent.openModal === "undefined") {
- location.href = "/app/#/explore";
+ location.href = "/app/#/home";
}
</script>
<meta charset="UTF-8">
diff --git a/app/ui/settings.php b/app/ui/settings.php
index 34cd095..3ef243d 100644
--- a/app/ui/settings.php
+++ b/app/ui/settings.php
@@ -1,4 +1,4 @@
-<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; ?>
+<?php header("X-Frame-Options: SAMEORIGIN"); require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $_PROFILE; ?>
<!doctype html>
<html lang="en">
<head>
@@ -26,11 +26,15 @@
<div class="container">
<br>
<h2 class="desktop-title" style="margin-top: 10px; margin-bottom: 20px; margin-left: 10px;">Settings</h2>
+
<div style="margin-left: 10px;">
+ <h5>Preferences</h5>
<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 highly efficient. If you use Bluetooth headphones, the difference should be unnoticeable.</div>
+ <label class="form-check-label" for="data-saving">
+ Enable data saving
+ <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>
+ </label>
</div>
<script>
if (localStorage.getItem("data-saving") === "true") document.getElementById("data-saving").checked = true;
@@ -42,8 +46,10 @@
<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>
+ <label class="form-check-label" for="normalize">
+ Normalize loudness
+ <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>
+ </label>
</div>
<script>
if (localStorage.getItem("normalize") === "true") document.getElementById("normalize").checked = true;
@@ -55,8 +61,10 @@
<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>
+ <label class="form-check-label" for="enable-stella">
+ Mist Stella
+ <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>
+ </label>
</div>
<script>
if (localStorage.getItem("show-stella-settings") === "true") document.getElementById("stella").style.display = "";
@@ -72,8 +80,10 @@
<?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>
+ <label class="form-check-label" for="desktop-notification">
+ Display notification when song changes
+ <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>
+ </label>
</div>
<script>
if (localStorage.getItem("desktop-notification") === "true") document.getElementById("desktop-notification").checked = true;
@@ -86,8 +96,10 @@
<?php if (str_contains($_SERVER['HTTP_USER_AGENT'], "MistNative/")): ?>
<div class="form-check form-switch" style="margin-top: 10px;">
<input onchange="saveRP();" class="form-check-input" type="checkbox" role="switch" id="rich-presence">
- <label class="form-check-label" for="desktop-notification">Show the song you are listening to on Discord</label>
- <div class="text-muted small">Using Discord Rich Presence, Mist can display on Discord the song you are currently listening to. You need to have the Discord desktop app installed and running on your computer for this to work.</div>
+ <label class="form-check-label" for="desktop-notification">
+ Show the song you are listening to on Discord
+ <div class="text-muted small">Using Discord Rich Presence, Mist can display on Discord the song you are currently listening to. You need to have the Discord desktop app installed and running on your computer for this to work.</div>
+ </label>
</div>
<script>
if (localStorage.getItem("rich-presence") === "true") document.getElementById("rich-presence").checked = true;
@@ -106,6 +118,120 @@
<?php endif; ?>
<hr>
+ <h5>Privacy</h5>
+
+ <div style="display: grid; grid-template-columns: 1fr max-content; grid-gap: 20px;">
+ <div>
+ <label for="privacy-profile">
+ Who can see your Mist profile?
+ <div class="text-muted small">Your Mist profile always shows some information about you that is publicly available on your Equestria.dev account. If you would like people not to know you are using Mist, you can change it here.</div>
+ </label>
+ </div>
+ <div>
+ <select disabled onchange="savePrivacy();" class="form-select" id="privacy-profile">
+ <option selected value="2">Everyone</option>
+ <option value="1">All Mist users</option>
+ <option value="0">Only me</option>
+ </select>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 1fr max-content; grid-gap: 20px; margin-top: 10px;">
+ <div>
+ <label for="privacy-library">
+ Who can see your library?
+ <div class="text-muted small">Your library can show up on your profile if you wish for it to show up. It will show all the albums and songs you have manually added to your library and not the songs you have only searched for.</div>
+ </label>
+ </div>
+ <div>
+ <select disabled onchange="savePrivacy();" class="form-select" id="privacy-library">
+ <option value="2">Everyone</option>
+ <option value="1">All Mist users</option>
+ <option selected value="0">Only me</option>
+ </select>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 1fr max-content; grid-gap: 20px; margin-top: 10px;">
+ <div>
+ <label for="privacy-library">
+ Who can see your history and activity?
+ <div class="text-muted small">If this is enabled, other people can see when you last used Mist and which songs you last listened to. Turning this on might reveal personal information, so be careful if you set this to "Everyone".</div>
+ </label>
+ </div>
+ <div>
+ <select disabled onchange="savePrivacy();" class="form-select" id="privacy-history">
+ <option value="2">Everyone</option>
+ <option value="1">All Mist users</option>
+ <option selected value="0">Only me</option>
+ </select>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 1fr max-content; grid-gap: 20px; margin-top: 10px;">
+ <div>
+ <label for="privacy-favorites">
+ Who can see your favorites?
+ <div class="text-muted small">Other people can see your favorites on your profile to know what songs you like. If you don't turn on the option below, other Mist users will not be able to directly listen to them in the Mist app.</div>
+ </label>
+ </div>
+ <div>
+ <select disabled onchange="savePrivacy();" class="form-select" id="privacy-favorites">
+ <option value="2">Everyone</option>
+ <option value="1">All Mist users</option>
+ <option selected value="0">Only me</option>
+ </select>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 1fr max-content; grid-gap: 20px; margin-top: 10px;">
+ <div>
+ <label for="privacy-listen">
+ Who can listen to your favorites?
+ <div class="text-muted small">If this is enabled, other Mist users will see your favorites directly in the application, giving them the option to listen to them if they want to. This means you won't have to share anything manually.</div>
+ </label>
+ </div>
+ <div>
+ <select disabled onchange="savePrivacy();" class="form-select" id="privacy-listen">
+ <option value="1">All Mist users</option>
+ <option selected value="0">Only me</option>
+ </select>
+ </div>
+ </div>
+ <div style="display: grid; grid-template-columns: 1fr max-content; grid-gap: 20px; margin-top: 10px;">
+ <div>
+ <label for="privacy-custom">
+ Who can see your profile customizations?
+ <div class="text-muted small">If you customize your Mist profile through the options provided inside the app, you can choose who will see these customizations. This includes your banner, color, description, and all other settings.</div>
+ </label>
+ </div>
+ <div>
+ <select disabled onchange="savePrivacy();" class="form-select" id="privacy-custom">
+ <option value="2">Everyone</option>
+ <option selected value="1">All Mist users</option>
+ <option value="0">Only me</option>
+ </select>
+ </div>
+ </div>
+
+ <hr>
+ <h5>Profile</h5>
+ <p><a target="_blank" href="/profile/?/<?= $_PROFILE["id"] ?>">View your profile</a> · <a target="_blank" href="#" id="profile-url-btn">Copy profile URL</a></p>
+ <div class="mb-3">
+ <label for="description" class="form-label">Profile description:</label>
+ <textarea onchange="saveCustom();" maxlength="500" class="form-control" id="description" rows="3" placeholder="You can enter some information about your musical tastes, your experience working with audio and music, the hardware you use, etc... Markdown is also supported. If the content in your profile is not safe for work, remember to check the corresponding box."></textarea>
+ </div>
+
+ <div class="mb-3">
+ <label for="banner" class="form-label">Profile banner:</label>
+ <input onchange="saveCustom();" maxlength="120" type="text" class="form-control" id="banner" placeholder="Enter an image URL or a Derpibooru URL">
+ </div>
+
+ <div class="form-check form-switch">
+ <input onchange="saveCustom();" class="form-check-input" type="checkbox" role="switch" id="nsfw">
+ <label class="form-check-label" for="nsfw">
+ Mark profile as not safe for work
+ <div class="text-muted small">If your profile contains adult or violent content, check this box. This will show a warning when users open your profile informing them about the content that can be found on it, and giving them the option to not read your profile if needed.</div>
+ </label>
+ </div>
+
+ <hr>
<?php if (str_contains($_SERVER['HTTP_USER_AGENT'], "MistNative/")): ?>
<a onclick="window.parent.MistNative.about();" href="#">About Mist</a>
<?php else: ?>
@@ -113,6 +239,7 @@
<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") ?>)<span id="copyright-separator-desktop"> · </span><span id="copyright-separator-mobile"><br></span>© <?= date('Y') ?> Equestria.dev</span>
</div>
+ <br><br>
<style>
@media (min-width: 768px) {
#copyright-separator-mobile {
@@ -126,7 +253,82 @@
}
}
</style>
- <?php endif; ?>
+ <?php endif; global $privacy; global $profile; ?>
+ <script>
+ async function saveCustom() {
+ document.getElementById("banner").disabled = true;
+ document.getElementById("description").disabled = true;
+ document.getElementById("nsfw").disabled = true;
+
+ let customData = {
+ nsfw: document.getElementById("nsfw").checked,
+ description: document.getElementById("description").value.trim().substring(0, 500),
+ url: document.getElementById("banner").value.trim().substring(0, 120)
+ }
+
+ console.log(customData);
+
+ let fd = new FormData();
+ fd.append('nsfw', customData.nsfw);
+ fd.append('description', customData.description);
+ fd.append('url', customData.url);
+
+ await fetch("/api/saveProfile.php", {
+ body: fd,
+ method: "post"
+ });
+
+ document.getElementById("banner").disabled = false;
+ document.getElementById("description").disabled = false;
+ document.getElementById("nsfw").disabled = false;
+ }
+
+ async function savePrivacy() {
+ document.getElementById("privacy-profile").disabled = true;
+ document.getElementById("privacy-library").disabled = true;
+ document.getElementById("privacy-history").disabled = true;
+ document.getElementById("privacy-favorites").disabled = true;
+ document.getElementById("privacy-listen").disabled = true;
+ document.getElementById("privacy-custom").disabled = true;
+
+ await fetch("/api/savePrivacy.php?profile=" + document.getElementById("privacy-profile").value + "&library=" + document.getElementById("privacy-library").value + "&history=" + document.getElementById("privacy-history").value + "&favorites=" + document.getElementById("privacy-favorites").value + "&listen=" + document.getElementById("privacy-listen").value + "&custom=" + document.getElementById("privacy-custom").value);
+
+ document.getElementById("privacy-profile").disabled = false;
+ document.getElementById("privacy-library").disabled = false;
+ document.getElementById("privacy-history").disabled = false;
+ document.getElementById("privacy-favorites").disabled = false;
+ document.getElementById("privacy-listen").disabled = false;
+ document.getElementById("privacy-custom").disabled = false;
+ }
+
+ function loadSettings(privacy, profile) {
+ window.privacySettings = privacy;
+ window.profileSettings = profile;
+
+ for (let item of Object.keys(privacy)) {
+ document.getElementById("privacy-" + item).value = privacy[item].toString();
+ }
+
+ document.getElementById("nsfw").checked = window.profileSettings.nsfw;
+ document.getElementById("description").value = window.profileSettings.description;
+ document.getElementById("banner").value = window.profileSettings.banner_orig ?? window.profileSettings.banner;
+
+ document.getElementById("privacy-profile").disabled = false;
+ document.getElementById("privacy-library").disabled = false;
+ document.getElementById("privacy-history").disabled = false;
+ document.getElementById("privacy-favorites").disabled = false;
+ document.getElementById("privacy-listen").disabled = false;
+ document.getElementById("privacy-custom").disabled = false;
+ }
+
+ document.getElementById("profile-url-btn").onclick = (e) => {
+ e.preventDefault();
+ navigator.clipboard.writeText("https://mist.equestria.horse/profile/?/<?= $_PROFILE["id"] ?>");
+ }
+
+ loadSettings(JSON.parse(`<?= json_encode($privacy) ?>`), JSON.parse(`<?= json_encode($profile) ?>`));
+ </script>
</div>
+ </div>
</body>
</html> \ No newline at end of file
diff --git a/app/ui/update.php b/app/ui/update.php
index b50dc21..a5776f3 100644
--- a/app/ui/update.php
+++ b/app/ui/update.php
@@ -27,7 +27,7 @@
<div style="text-align: center;">
<?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>
+ <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")) ?>'); window.parent._modal.hide();">Continue</a>
</div>
</div>
diff --git a/app/ui/welcome-dp.php b/app/ui/welcome-dp.php
deleted file mode 100644
index 3986ca3..0000000
--- a/app/ui/welcome-dp.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?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/app/ui/welcome.php b/app/ui/welcome.php
index 800da74..9ca0d38 100644
--- a/app/ui/welcome.php
+++ b/app/ui/welcome.php
@@ -33,7 +33,7 @@
<p style="margin-top: 100px;" class="small text-muted">Your searches, favorites and library help improve the service. The administrators can provide you with more information about how your data is managed.</p>
<p class="small text-muted">With your Equestria.dev Account, you will be able to sign in to available services. Equestria.dev records certain data for security, support and reporting purposes. <a target="_blank" href="https://equestria.dev/legal/privacy">See how this data is managed.</a></p>
- <a style="margin-bottom: 30px; display: block;" class="btn btn-primary" onclick="localStorage.setItem('welcomed', 'true'); 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>
+ <a style="margin-bottom: 30px; display: block;" class="btn btn-primary" onclick="localStorage.setItem('welcomed', 'true'); localStorage.setItem('lastUpdate', '<?= trim(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/version")) ?>'); window.parent._modal.hide();">Continue</a>
</div>
</div>
diff --git a/assets/.DS_Store b/assets/.DS_Store
index 5ab4458..45c9d8c 100644
--- a/assets/.DS_Store
+++ b/assets/.DS_Store
Binary files differ
diff --git a/assets/dark.css b/assets/dark.css
index 18e5855..c1cd24b 100644
--- a/assets/dark.css
+++ b/assets/dark.css
@@ -12,6 +12,10 @@
background-color: black !important;
}
+ .track:hover {
+ background-color: #151515 !important;
+ }
+
body {
color: white !important;
}
@@ -69,6 +73,10 @@
color: #aaa !important;
}
+ #favorites-user-select {
+ filter: invert(1) !important;
+ }
+
#filter, #search, .btn, .form-check-input, .link, .btn-close {
filter: invert(1) hue-rotate(180deg);
}
diff --git a/assets/icons/home.svg b/assets/icons/home.svg
new file mode 100644
index 0000000..0887f2c
--- /dev/null
+++ b/assets/icons/home.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="M242-200h131v-247h214v247h131v-358L480-738 242-558v358Zm-54 54v-439l292-220 292 220v439H533v-247H427v247H188Zm292-323Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/search.svg b/assets/icons/search.svg
new file mode 100644
index 0000000..f812a6f
--- /dev/null
+++ b/assets/icons/search.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M787-138 535-390q-30 25-73.5 38.5T379-338q-102.231 0-173.115-70.837Q135-479.675 135-581.837 135-684 205.837-755q70.838-71 173-71Q481-826 552-755.115 623-684.231 623-582q0 42-13.5 83T572-429l253 253-38 38ZM379-392q81 0 135.5-54.5T569-582q0-81-54.5-135.5T379-772q-81 0-135.5 54.5T189-582q0 81 54.5 135.5T379-392Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/volume-down.svg b/assets/icons/volume-down.svg
new file mode 100644
index 0000000..f60256d
--- /dev/null
+++ b/assets/icons/volume-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M310-392v-176h144l154-156v488L454-392H310Zm54-54h112l78 80v-228l-78 80H364v68Zm95-34Z"/></svg> \ No newline at end of file
diff --git a/assets/icons/volume-up.svg b/assets/icons/volume-up.svg
new file mode 100644
index 0000000..d97dfa6
--- /dev/null
+++ b/assets/icons/volume-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M548-121v-56q101-30 164.5-115T776-482q0-104-64-188T548-784v-56q123 31 202.5 131T830-482q0 128-79.5 229T548-121ZM130-392v-176h144l154-156v488L274-392H130Zm418 48v-272q34 23 53 59t19 77q0 42-19.5 77.5T548-344ZM374-594l-78 80H184v68h112l78 80v-228Zm-95 114Z"/></svg> \ No newline at end of file
diff --git a/assets/styles.css b/assets/styles.css
index 0ad2afc..890a13e 100644
--- a/assets/styles.css
+++ b/assets/styles.css
@@ -136,13 +136,30 @@ body.native #lyrics-page {
}
}
+.form-range::-webkit-slider-thumb {
+ background-color: #ccc;
+ box-shadow: none !important;
+}
+
+.form-range::-webkit-slider-thumb:active {
+ background-color: #eee;
+}
+
@media (max-width: 863px) {
#player.bg-white.desktop-player.mobilified .container {
grid-template-columns: 1fr !important;
}
+ #player.mobilified #badges {
+ display: none !important;
+ }
+
#player.desktop-player.mobilified .player-badge-desktop {
- display: initial !important;
+ display: none !important;
+ }
+
+ #player.desktop-player.mobilified #info-grid-title-inner {
+ max-width: initial !important;
}
#player.desktop-player.mobilified .player-btn {
@@ -347,42 +364,6 @@ body.native #lyrics-page {
margin-right: 5px;
}
-#player #badge-hires .player-badge-desktop {
- background: white;
- color: rgb(182, 110, 2);
- padding-left: 5px;
- margin: -2px -5px -2px 5px;
- display: flex;
- align-items: center;
- padding-right: 5px;
- border-top-right-radius: 5px;
- border-bottom-right-radius: 5px;
-}
-
-#player #badge-cd .player-badge-desktop {
- background: white;
- color: #02b6a7;
- padding-left: 5px;
- margin: -2px -5px -2px 5px;
- display: flex;
- align-items: center;
- padding-right: 5px;
- border-top-right-radius: 5px;
- border-bottom-right-radius: 5px;
-}
-
-#player #badge-lossy .player-badge-desktop {
- background: white;
- color: #a402b6;
- padding-left: 5px;
- margin: -2px -5px -2px 5px;
- display: flex;
- align-items: center;
- padding-right: 5px;
- border-top-right-radius: 5px;
- border-bottom-right-radius: 5px;
-}
-
.navigation-item img {
filter: brightness(0%);
}
@@ -513,4 +494,16 @@ body.web {
.tooltip {
z-index: 99999 !important;
+}
+
+#player.mobilified #info-grid {
+ z-index: initial !important;
+}
+
+#buttons {
+ display: block !important;
+}
+
+.track:hover {
+ background-color: #f2f2f2;
} \ No newline at end of file
diff --git a/icons/adult.svg b/icons/adult.svg
new file mode 100644
index 0000000..affe2c9
--- /dev/null
+++ b/icons/adult.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-383q6-30-2-58.5T776-496q-24 19-41.5 42.5T710-399q-6 29 1.5 58t21.5 55q26-18 43-43t24-54ZM206-625q21 20 50.5 24t52.5-10l-30-30q-22-22-50-25t-53 11l30 30Zm74-183-95 95q35-11 70-1.5t61 35.5l31 31q16-24 12.5-54T334-753l-54-55Zm538 704L351-570q-43 28-94 24.5T168-587l-92-93 204-205 92 94q37 37 42 89t-25 95l270 269q-5-19-6-37t4-37q6-27 18-51t31-44q19-20 40-37t46-31q33 45 52.5 96.5T852-373q-8 43-32.5 77.5T761-237l96 96-39 37ZM296-666Zm322 146-39-37 221-222v-28h-28L551-586l-38-39 237-236h104v104L618-520ZM148-128l-29-29q-13-13-13-30t13-30l116-115-88-88 6-6q22-22 51.5-25t54.5 13l34 33 41-41 38 39-40 40 29 29 40-40 38 37-41 41 32 33q15 25 12 54t-25 51l-6 6-88-88-115 116q-13 13-30 13t-30-13Z"/></svg> \ No newline at end of file
diff --git a/icons/logo-transparent.svg b/icons/logo-transparent.svg
new file mode 100644
index 0000000..734922c
--- /dev/null
+++ b/icons/logo-transparent.svg
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#49008D;}
+ .st1{fill:#FE6972;}
+ .st2{fill:#D200AB;}
+ .st3{fill:#FD6872;}
+ .st4{fill:#FD6774;}
+ .st5{fill:#48426B;}
+ .st6{fill:#4D426A;}
+ .st7{fill:#4B4168;}
+</style>
+<g>
+ <path class="st0" d="M281.6,222.7c13.9-18.5,28.2-36.7,45.6-52.1c15.6-13.7,32.3-25.6,53-30.4c7.2-1.7,14.3-1.9,21.6-0.4
+ c10.6,2.2,17.5,9.9,18.8,20.6c1.1,8.9-0.6,17.6-2.5,26.2c-5.5,25.9-11.3,51.7-22.1,76c-3.4,7.5-7.3,14.8-13,20.9
+ c-3.7,3.9-7.7,7.3-13,8.7c-2.1,2.8-4.6,1.6-6.8,0.5c-19.4-9.6-40.4-10.5-61.4-11.1c-4.5-0.1-9-0.1-13.5,0c-2.5,0.1-4.7-0.5-6.5-2.3
+ c-1.6-1.9-2.3-4.2-2.5-6.5c-0.6-8.5-3.7-16.4-5.7-24.5c-1.1-4.7-1.1-8.9,2.1-12.7c1.9-2.2,2.3-4.9,2.8-7.6
+ C279,225.8,279.2,223.6,281.6,222.7z"/>
+ <path class="st0" d="M131.7,294.8c-4.4-2.1-8.7-4.4-11.3-8.8c-8.4-11.8-15-24.6-20.1-38.2c-4.3-11.4-7.7-23-9.8-34.9
+ c-1.5-8.5-3.3-16.9-5.8-25.1c-2.7-9.2-3.5-18.3-1.9-27.6c2.3-9.8,9.5-13.9,18.6-15.5c14.6-2.5,28.4,0.5,41.7,6.9
+ c20.3,9.8,36.8,24.4,52.1,40.5c9.5,9.9,17.8,20.9,26.5,31.5c0.2,0,0.5,0,0.6,0.1c2.5,9.5,12.6,16.9,7.4,28.7
+ c-2.3,5.3-3.5,11.2-4,17.1c-0.3,3.2-0.4,6.7-2.8,9.4c-2.6,2.5-6,2.5-9.3,2.6c-18.6,0.3-37,1.4-55,6.2c-6.6,1.7-12.8,4.6-19.1,7.1
+ C136.7,295.7,134.2,297.4,131.7,294.8z"/>
+ <path class="st1" d="M221.8,277.7c1.1-10.9,2.8-21.6,7.3-31.7c0.6-1.2,1.6-2.8,0.1-3.7c-6.8-4.6-6.5-11.9-7.7-18.7
+ c3.6-7.6,6.5-15.6,14.1-20.5c1.5-0.9,1.4-2.6,0.9-4.2c-4.4-18.1-12.3-34.3-25.2-47.9c-3-3.2-6.4-5.3-11-6
+ c-9.7-1.4-15.4-9-14.9-18.8c0.3-6.6,5.6-10.2,11.9-7.9c7,2.6,11.4,7.8,12.6,15c1,5.9,3.8,10.3,7.9,14.6
+ c12.4,12.8,20.4,28.2,24.8,45.5c1.2,4.7,3.2,6.9,8.1,5.5c1.1-0.3,2.4-0.3,3.5-0.1c4.4,1,5.8-1.1,6.6-5.1
+ c3.5-17.7,10.9-33.6,23.3-46.8c3.9-4.2,6.9-8.2,8.2-14.2c1.7-7.6,7.5-12.7,15.6-14.2c4.4-0.8,8.5-0.3,11,3.6
+ c2.6,4.1,1.7,8.4-1.2,12c-4.8,5.8-10.9,9.7-18.4,10.7c-3,0.4-5.1,1.6-7,3.7c-13.5,14.5-20.8,32.1-24.7,51.2c-0.4,2-0.3,4,1.5,5.4
+ c6,4.6,9.2,11.1,12.5,17.5c-0.5,6.3-1.1,12.5-5.9,17.3c-1.2,1.2-1,2.9-0.5,4.4c3.5,11.4,6.1,23.1,8.3,34.9
+ c3.7,4.2,3.1,9.6,3.5,14.5c1.2,17.1-1.3,33.9-6,50.3c-0.4,1.5-0.9,3-2.6,3.7c-2.1,1.1-2.5,3.4-3.5,5.2c-2.1,3.5-4.2,7-7.1,10
+ c-8.8,9-18.8,9.1-27.6,0c-3.2-3.4-6.1-7.1-9.1-10.7c-2.9-0.9-3.3-3.6-4.1-6c-6.7-19.5-8.9-39.5-8.2-60
+ C218.9,283,220.1,280.4,221.8,277.7z"/>
+ <path class="st2" d="M221.8,277.7c-1.6,25.4,0.9,50.4,9.3,74.6c-2,11.8-3.8,23.6-10.4,34.1c-7.1,11.4-17.3,16.6-30.6,16.1
+ c-9.4-0.4-14.4-4.4-16.8-13.5c-2.6-9.9-3.8-20-3.3-30.3c0.4-8.8,0.4-8.9-8-5.2c-8.1,3.5-16.5,6.3-25.1,8.2
+ c-4.3,0.9-8.6,1.1-12.8,0.2c-10.2-2.2-15.8-10-14.1-20.3c3-17.5,9.2-33.6,21.7-46.7c7.6-2.9,15.1-6.2,22.9-8.6
+ c19.4-6,39.5-7.4,59.8-7.4C216.9,278.7,219.5,279.1,221.8,277.7z"/>
+ <path class="st2" d="M278.4,347.8c0.2-2.1-0.1-4.4,0.5-6.4c6-19.4,7-39.2,4.3-59.2c-0.1-0.9,0.2-1.9,0.3-2.9
+ c22.1-0.9,44.1-0.4,65.8,5.1c7.2,1.8,13.4,6.3,20.7,7.8c3.6,5.6,9,9.8,12.4,15.5c4.7,7.8,8.6,15.9,9.9,25c2.2,14.9-6.1,24-21,23
+ c-10.5-0.7-20.4-3.8-29.9-8.2c-5.8-2.6-5.7-2.7-5.3,3.5c0.5,10.2,0.9,20.3-1.1,30.5c-1.7,8.5-7.1,12.9-15.1,14.2
+ c-9.3,1.5-17.4-0.9-24.2-7.6C284.4,376.9,279.6,363.1,278.4,347.8z"/>
+ <path class="st2" d="M356.7,256.6c-9.3,2-21,1.6-32.8,0.6c-3.3-0.3-4.9-2-5.1-5.3c-0.8-15.4,0-30.5,5.2-45.2
+ c2.6-7.4,8.4-11.1,16-12.1c5.3-0.8,10.1,2.5,11.7,7.6c1.3,4.2,0.8,8.5,0.3,12.8c-1,8-0.9,8,6.7,6c3.6-1,7.2-1.9,11-1.8
+ c7.8,0.2,11.8,4.8,10.7,12.6C378.9,242.5,369.4,253.2,356.7,256.6z"/>
+ <path class="st3" d="M367.2,197c-2.5-9.6,4.7-22.5,14.2-25.4c6.5-2,11.8,1.1,13.1,7.8c1.9,9.4-5.6,21.9-14.8,24.7
+ C373.8,205.9,368.8,203,367.2,197z"/>
+ <path class="st2" d="M180.4,219.1c2.4,11.6,3.8,23.4,2.9,35.5c-0.2,3.4-1.7,5-4.9,5.3c-10.7,1-21.4,2.2-32.2,0.4
+ c-12.4-2.1-23.3-13.3-24.9-25.8c-0.8-6.5,3.3-11.1,10.6-10.9c4.5,0.1,9,0.8,13.3,1.9c4.1,1.1,5,0,4.5-3.8
+ c-0.5-3.9-1.2-7.8-1.1-11.7c0.4-10,8.1-14.6,17.6-10.8C175.1,202.8,177.6,210.9,180.4,219.1z"/>
+ <path class="st4" d="M134.8,193.6c0.4,1.2,0.7,4.1,0.1,7c-1.5,6.8-7.4,10-13.9,7.4c-7.8-3.1-14.4-13.1-14.2-21.4
+ c0.2-9.4,8.2-13.9,16.4-9.2C128.9,180.8,132.7,185.6,134.8,193.6z"/>
+ <path class="st5" d="M268.3,304c0.4,1.1,0.2,2-0.6,2.8c-1,1-2.2,0.8-3.2,0c-7.2-6.1-14.3-4.8-21.4,0.3c-1.1,0.8-2.5,1.2-3.7-0.1
+ c-0.9-1.1-0.5-2.1,0.1-3.2c4.6-7.8,23.7-8.2,28.3-0.6C268,303.4,268.1,303.8,268.3,304z"/>
+ <path class="st6" d="M250.9,274.9c4.9-1.1,9.3,0.9,12.9,5c0.9,1.1,1.7,2.4,0.7,3.7c-1.1,1.5-2.4,0.6-3.5-0.3
+ c-5.1-4.4-10.1-4.3-15.1,0.2c-1,0.9-2.2,2.1-3.5,0.9c-1.4-1.3-0.7-2.9,0.3-4.2C244.7,277.7,247.1,275.9,250.9,274.9z"/>
+ <path class="st7" d="M265.3,328.2c0.2,1.2,0.5,2.2-0.5,2.9c-1,0.7-1.9,0.3-2.6-0.4c-5.6-5.3-10.9-4.2-16,0.6
+ c-0.7,0.7-1.7,1.1-2.7,0.6c-1-0.5-1.1-1.5-0.9-2.5c0.7-3.3,7.1-7.5,11.8-7.5C259.3,322,263,324.2,265.3,328.2z"/>
+</g>
+</svg>
diff --git a/includes/Parsedown.php b/includes/Parsedown.php
new file mode 100644
index 0000000..3e29589
--- /dev/null
+++ b/includes/Parsedown.php
@@ -0,0 +1,1994 @@
+<?php
+
+#
+#
+# Parsedown
+# http://parsedown.org
+#
+# (c) Emanuil Rusev
+# http://erusev.com
+#
+# For the full license information, view the LICENSE file that was distributed
+# with this source code.
+#
+#
+
+class Parsedown
+{
+ # ~
+
+ const version = '1.8.0-beta-7';
+
+ # ~
+
+ function text($text)
+ {
+ $Elements = $this->textElements($text);
+
+ # convert to markup
+ $markup = $this->elements($Elements);
+
+ # trim line breaks
+ $markup = trim($markup, "\n");
+
+ return $markup;
+ }
+
+ protected function textElements($text)
+ {
+ # make sure no definitions are set
+ $this->DefinitionData = array();
+
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ # remove surrounding line breaks
+ $text = trim($text, "\n");
+
+ # split text into lines
+ $lines = explode("\n", $text);
+
+ # iterate through lines to identify blocks
+ return $this->linesElements($lines);
+ }
+
+ #
+ # Setters
+ #
+
+ function setBreaksEnabled($breaksEnabled)
+ {
+ $this->breaksEnabled = $breaksEnabled;
+
+ return $this;
+ }
+
+ protected $breaksEnabled;
+
+ function setMarkupEscaped($markupEscaped)
+ {
+ $this->markupEscaped = $markupEscaped;
+
+ return $this;
+ }
+
+ protected $markupEscaped;
+
+ function setUrlsLinked($urlsLinked)
+ {
+ $this->urlsLinked = $urlsLinked;
+
+ return $this;
+ }
+
+ protected $urlsLinked = true;
+
+ function setSafeMode($safeMode)
+ {
+ $this->safeMode = (bool) $safeMode;
+
+ return $this;
+ }
+
+ protected $safeMode;
+
+ function setStrictMode($strictMode)
+ {
+ $this->strictMode = (bool) $strictMode;
+
+ return $this;
+ }
+
+ protected $strictMode;
+
+ protected $safeLinksWhitelist = array(
+ 'http://',
+ 'https://',
+ 'ftp://',
+ 'ftps://',
+ 'mailto:',
+ 'tel:',
+ 'data:image/png;base64,',
+ 'data:image/gif;base64,',
+ 'data:image/jpeg;base64,',
+ 'irc:',
+ 'ircs:',
+ 'git:',
+ 'ssh:',
+ 'news:',
+ 'steam:',
+ );
+
+ #
+ # Lines
+ #
+
+ protected $BlockTypes = array(
+ '#' => array('Header'),
+ '*' => array('Rule', 'List'),
+ '+' => array('List'),
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
+ '0' => array('List'),
+ '1' => array('List'),
+ '2' => array('List'),
+ '3' => array('List'),
+ '4' => array('List'),
+ '5' => array('List'),
+ '6' => array('List'),
+ '7' => array('List'),
+ '8' => array('List'),
+ '9' => array('List'),
+ ':' => array('Table'),
+ '<' => array('Comment', 'Markup'),
+ '=' => array('SetextHeader'),
+ '>' => array('Quote'),
+ '[' => array('Reference'),
+ '_' => array('Rule'),
+ '`' => array('FencedCode'),
+ '|' => array('Table'),
+ '~' => array('FencedCode'),
+ );
+
+ # ~
+
+ protected $unmarkedBlockTypes = array(
+ 'Code',
+ );
+
+ #
+ # Blocks
+ #
+
+ protected function lines(array $lines)
+ {
+ return $this->elements($this->linesElements($lines));
+ }
+
+ protected function linesElements(array $lines)
+ {
+ $Elements = array();
+ $CurrentBlock = null;
+
+ foreach ($lines as $line)
+ {
+ if (chop($line) === '')
+ {
+ if (isset($CurrentBlock))
+ {
+ $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
+ ? $CurrentBlock['interrupted'] + 1 : 1
+ );
+ }
+
+ continue;
+ }
+
+ while (($beforeTab = strstr($line, "\t", true)) !== false)
+ {
+ $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
+
+ $line = $beforeTab
+ . str_repeat(' ', $shortage)
+ . substr($line, strlen($beforeTab) + 1)
+ ;
+ }
+
+ $indent = strspn($line, ' ');
+
+ $text = $indent > 0 ? substr($line, $indent) : $line;
+
+ # ~
+
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']))
+ {
+ $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
+ $Block = $this->$methodName($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $CurrentBlock = $Block;
+
+ continue;
+ }
+ else
+ {
+ if ($this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
+ $CurrentBlock = $this->$methodName($CurrentBlock);
+ }
+ }
+ }
+
+ # ~
+
+ $marker = $text[0];
+
+ # ~
+
+ $blockTypes = $this->unmarkedBlockTypes;
+
+ if (isset($this->BlockTypes[$marker]))
+ {
+ foreach ($this->BlockTypes[$marker] as $blockType)
+ {
+ $blockTypes []= $blockType;
+ }
+ }
+
+ #
+ # ~
+
+ foreach ($blockTypes as $blockType)
+ {
+ $Block = $this->{"block$blockType"}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $Block['type'] = $blockType;
+
+ if ( ! isset($Block['identified']))
+ {
+ if (isset($CurrentBlock))
+ {
+ $Elements[] = $this->extractElement($CurrentBlock);
+ }
+
+ $Block['identified'] = true;
+ }
+
+ if ($this->isBlockContinuable($blockType))
+ {
+ $Block['continuable'] = true;
+ }
+
+ $CurrentBlock = $Block;
+
+ continue 2;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph')
+ {
+ $Block = $this->paragraphContinue($Line, $CurrentBlock);
+ }
+
+ if (isset($Block))
+ {
+ $CurrentBlock = $Block;
+ }
+ else
+ {
+ if (isset($CurrentBlock))
+ {
+ $Elements[] = $this->extractElement($CurrentBlock);
+ }
+
+ $CurrentBlock = $this->paragraph($Line);
+
+ $CurrentBlock['identified'] = true;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
+ $CurrentBlock = $this->$methodName($CurrentBlock);
+ }
+
+ # ~
+
+ if (isset($CurrentBlock))
+ {
+ $Elements[] = $this->extractElement($CurrentBlock);
+ }
+
+ # ~
+
+ return $Elements;
+ }
+
+ protected function extractElement(array $Component)
+ {
+ if ( ! isset($Component['element']))
+ {
+ if (isset($Component['markup']))
+ {
+ $Component['element'] = array('rawHtml' => $Component['markup']);
+ }
+ elseif (isset($Component['hidden']))
+ {
+ $Component['element'] = array();
+ }
+ }
+
+ return $Component['element'];
+ }
+
+ protected function isBlockContinuable($Type)
+ {
+ return method_exists($this, 'block' . $Type . 'Continue');
+ }
+
+ protected function isBlockCompletable($Type)
+ {
+ return method_exists($this, 'block' . $Type . 'Complete');
+ }
+
+ #
+ # Code
+
+ protected function blockCode($Line, $Block = null)
+ {
+ if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['indent'] >= 4)
+ {
+ $text = substr($Line['body'], 4);
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'pre',
+ 'element' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeContinue($Line, $Block)
+ {
+ if ($Line['indent'] >= 4)
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['element']['text'] .= "\n";
+
+ $text = substr($Line['body'], 4);
+
+ $Block['element']['element']['text'] .= $text;
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeComplete($Block)
+ {
+ return $Block;
+ }
+
+ #
+ # Comment
+
+ protected function blockComment($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (strpos($Line['text'], '<!--') === 0)
+ {
+ $Block = array(
+ 'element' => array(
+ 'rawHtml' => $Line['body'],
+ 'autobreak' => true,
+ ),
+ );
+
+ if (strpos($Line['text'], '-->') !== false)
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockCommentContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ $Block['element']['rawHtml'] .= "\n" . $Line['body'];
+
+ if (strpos($Line['text'], '-->') !== false)
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+
+ #
+ # Fenced Code
+
+ protected function blockFencedCode($Line)
+ {
+ $marker = $Line['text'][0];
+
+ $openerLength = strspn($Line['text'], $marker);
+
+ if ($openerLength < 3)
+ {
+ return;
+ }
+
+ $infostring = trim(substr($Line['text'], $openerLength), "\t ");
+
+ if (strpos($infostring, '`') !== false)
+ {
+ return;
+ }
+
+ $Element = array(
+ 'name' => 'code',
+ 'text' => '',
+ );
+
+ if ($infostring !== '')
+ {
+ /**
+ * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
+ * Every HTML element may have a class attribute specified.
+ * The attribute, if specified, must have a value that is a set
+ * of space-separated tokens representing the various classes
+ * that the element belongs to.
+ * [...]
+ * The space characters, for the purposes of this specification,
+ * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
+ * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
+ * U+000D CARRIAGE RETURN (CR).
+ */
+ $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r"));
+
+ $Element['attributes'] = array('class' => "language-$language");
+ }
+
+ $Block = array(
+ 'char' => $marker,
+ 'openerLength' => $openerLength,
+ 'element' => array(
+ 'name' => 'pre',
+ 'element' => $Element,
+ ),
+ );
+
+ return $Block;
+ }
+
+ protected function blockFencedCodeContinue($Line, $Block)
+ {
+ if (isset($Block['complete']))
+ {
+ return;
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
+
+ unset($Block['interrupted']);
+ }
+
+ if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
+ and chop(substr($Line['text'], $len), ' ') === ''
+ ) {
+ $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
+
+ $Block['complete'] = true;
+
+ return $Block;
+ }
+
+ $Block['element']['element']['text'] .= "\n" . $Line['body'];
+
+ return $Block;
+ }
+
+ protected function blockFencedCodeComplete($Block)
+ {
+ return $Block;
+ }
+
+ #
+ # Header
+
+ protected function blockHeader($Line)
+ {
+ $level = strspn($Line['text'], '#');
+
+ if ($level > 6)
+ {
+ return;
+ }
+
+ $text = trim($Line['text'], '#');
+
+ if ($this->strictMode and isset($text[0]) and $text[0] !== ' ')
+ {
+ return;
+ }
+
+ $text = trim($text, ' ');
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'h' . $level,
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $text,
+ 'destination' => 'elements',
+ )
+ ),
+ );
+
+ return $Block;
+ }
+
+ #
+ # List
+
+ protected function blockList($Line, array $CurrentBlock = null)
+ {
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]');
+
+ if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches))
+ {
+ $contentIndent = strlen($matches[2]);
+
+ if ($contentIndent >= 5)
+ {
+ $contentIndent -= 1;
+ $matches[1] = substr($matches[1], 0, -$contentIndent);
+ $matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
+ }
+ elseif ($contentIndent === 0)
+ {
+ $matches[1] .= ' ';
+ }
+
+ $markerWithoutWhitespace = strstr($matches[1], ' ', true);
+
+ $Block = array(
+ 'indent' => $Line['indent'],
+ 'pattern' => $pattern,
+ 'data' => array(
+ 'type' => $name,
+ 'marker' => $matches[1],
+ 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
+ ),
+ 'element' => array(
+ 'name' => $name,
+ 'elements' => array(),
+ ),
+ );
+ $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
+
+ if ($name === 'ol')
+ {
+ $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
+
+ if ($listStart !== '1')
+ {
+ if (
+ isset($CurrentBlock)
+ and $CurrentBlock['type'] === 'Paragraph'
+ and ! isset($CurrentBlock['interrupted'])
+ ) {
+ return;
+ }
+
+ $Block['element']['attributes'] = array('start' => $listStart);
+ }
+ }
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => array(
+ 'function' => 'li',
+ 'argument' => !empty($matches[3]) ? array($matches[3]) : array(),
+ 'destination' => 'elements'
+ )
+ );
+
+ $Block['element']['elements'] []= & $Block['li'];
+
+ return $Block;
+ }
+ }
+
+ protected function blockListContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument']))
+ {
+ return null;
+ }
+
+ $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
+
+ if ($Line['indent'] < $requiredIndent
+ and (
+ (
+ $Block['data']['type'] === 'ol'
+ and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
+ ) or (
+ $Block['data']['type'] === 'ul'
+ and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
+ )
+ )
+ ) {
+ if (isset($Block['interrupted']))
+ {
+ $Block['li']['handler']['argument'] []= '';
+
+ $Block['loose'] = true;
+
+ unset($Block['interrupted']);
+ }
+
+ unset($Block['li']);
+
+ $text = isset($matches[1]) ? $matches[1] : '';
+
+ $Block['indent'] = $Line['indent'];
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => array(
+ 'function' => 'li',
+ 'argument' => array($text),
+ 'destination' => 'elements'
+ )
+ );
+
+ $Block['element']['elements'] []= & $Block['li'];
+
+ return $Block;
+ }
+ elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line))
+ {
+ return null;
+ }
+
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
+ {
+ return $Block;
+ }
+
+ if ($Line['indent'] >= $requiredIndent)
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['li']['handler']['argument'] []= '';
+
+ $Block['loose'] = true;
+
+ unset($Block['interrupted']);
+ }
+
+ $text = substr($Line['body'], $requiredIndent);
+
+ $Block['li']['handler']['argument'] []= $text;
+
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
+
+ $Block['li']['handler']['argument'] []= $text;
+
+ return $Block;
+ }
+ }
+
+ protected function blockListComplete(array $Block)
+ {
+ if (isset($Block['loose']))
+ {
+ foreach ($Block['element']['elements'] as &$li)
+ {
+ if (end($li['handler']['argument']) !== '')
+ {
+ $li['handler']['argument'] []= '';
+ }
+ }
+ }
+
+ return $Block;
+ }
+
+ #
+ # Quote
+
+ protected function blockQuote($Line)
+ {
+ if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'blockquote',
+ 'handler' => array(
+ 'function' => 'linesElements',
+ 'argument' => (array) $matches[1],
+ 'destination' => 'elements',
+ )
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockQuoteContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
+ {
+ $Block['element']['handler']['argument'] []= $matches[1];
+
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $Block['element']['handler']['argument'] []= $Line['text'];
+
+ return $Block;
+ }
+ }
+
+ #
+ # Rule
+
+ protected function blockRule($Line)
+ {
+ $marker = $Line['text'][0];
+
+ if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '')
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'hr',
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Setext
+
+ protected function blockSetextHeader($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '')
+ {
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+ return $Block;
+ }
+ }
+
+ #
+ # Markup
+
+ protected function blockMarkup($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches))
+ {
+ $element = strtolower($matches[1]);
+
+ if (in_array($element, $this->textLevelElements))
+ {
+ return;
+ }
+
+ $Block = array(
+ 'name' => $matches[1],
+ 'element' => array(
+ 'rawHtml' => $Line['text'],
+ 'autobreak' => true,
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockMarkupContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ $Block['element']['rawHtml'] .= "\n" . $Line['body'];
+
+ return $Block;
+ }
+
+ #
+ # Reference
+
+ protected function blockReference($Line)
+ {
+ if (strpos($Line['text'], ']') !== false
+ and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
+ ) {
+ $id = strtolower($matches[1]);
+
+ $Data = array(
+ 'url' => $matches[2],
+ 'title' => isset($matches[3]) ? $matches[3] : null,
+ );
+
+ $this->DefinitionData['Reference'][$id] = $Data;
+
+ $Block = array(
+ 'element' => array(),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Table
+
+ protected function blockTable($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (
+ strpos($Block['element']['handler']['argument'], '|') === false
+ and strpos($Line['text'], '|') === false
+ and strpos($Line['text'], ':') === false
+ or strpos($Block['element']['handler']['argument'], "\n") !== false
+ ) {
+ return;
+ }
+
+ if (chop($Line['text'], ' -:|') !== '')
+ {
+ return;
+ }
+
+ $alignments = array();
+
+ $divider = $Line['text'];
+
+ $divider = trim($divider);
+ $divider = trim($divider, '|');
+
+ $dividerCells = explode('|', $divider);
+
+ foreach ($dividerCells as $dividerCell)
+ {
+ $dividerCell = trim($dividerCell);
+
+ if ($dividerCell === '')
+ {
+ return;
+ }
+
+ $alignment = null;
+
+ if ($dividerCell[0] === ':')
+ {
+ $alignment = 'left';
+ }
+
+ if (substr($dividerCell, - 1) === ':')
+ {
+ $alignment = $alignment === 'left' ? 'center' : 'right';
+ }
+
+ $alignments []= $alignment;
+ }
+
+ # ~
+
+ $HeaderElements = array();
+
+ $header = $Block['element']['handler']['argument'];
+
+ $header = trim($header);
+ $header = trim($header, '|');
+
+ $headerCells = explode('|', $header);
+
+ if (count($headerCells) !== count($alignments))
+ {
+ return;
+ }
+
+ foreach ($headerCells as $index => $headerCell)
+ {
+ $headerCell = trim($headerCell);
+
+ $HeaderElement = array(
+ 'name' => 'th',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $headerCell,
+ 'destination' => 'elements',
+ )
+ );
+
+ if (isset($alignments[$index]))
+ {
+ $alignment = $alignments[$index];
+
+ $HeaderElement['attributes'] = array(
+ 'style' => "text-align: $alignment;",
+ );
+ }
+
+ $HeaderElements []= $HeaderElement;
+ }
+
+ # ~
+
+ $Block = array(
+ 'alignments' => $alignments,
+ 'identified' => true,
+ 'element' => array(
+ 'name' => 'table',
+ 'elements' => array(),
+ ),
+ );
+
+ $Block['element']['elements'] []= array(
+ 'name' => 'thead',
+ );
+
+ $Block['element']['elements'] []= array(
+ 'name' => 'tbody',
+ 'elements' => array(),
+ );
+
+ $Block['element']['elements'][0]['elements'] []= array(
+ 'name' => 'tr',
+ 'elements' => $HeaderElements,
+ );
+
+ return $Block;
+ }
+
+ protected function blockTableContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|'))
+ {
+ $Elements = array();
+
+ $row = $Line['text'];
+
+ $row = trim($row);
+ $row = trim($row, '|');
+
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
+
+ $cells = array_slice($matches[0], 0, count($Block['alignments']));
+
+ foreach ($cells as $index => $cell)
+ {
+ $cell = trim($cell);
+
+ $Element = array(
+ 'name' => 'td',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $cell,
+ 'destination' => 'elements',
+ )
+ );
+
+ if (isset($Block['alignments'][$index]))
+ {
+ $Element['attributes'] = array(
+ 'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
+ );
+ }
+
+ $Elements []= $Element;
+ }
+
+ $Element = array(
+ 'name' => 'tr',
+ 'elements' => $Elements,
+ );
+
+ $Block['element']['elements'][1]['elements'] []= $Element;
+
+ return $Block;
+ }
+ }
+
+ #
+ # ~
+ #
+
+ protected function paragraph($Line)
+ {
+ return array(
+ 'type' => 'Paragraph',
+ 'element' => array(
+ 'name' => 'p',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $Line['text'],
+ 'destination' => 'elements',
+ ),
+ ),
+ );
+ }
+
+ protected function paragraphContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ $Block['element']['handler']['argument'] .= "\n".$Line['text'];
+
+ return $Block;
+ }
+
+ #
+ # Inline Elements
+ #
+
+ protected $InlineTypes = array(
+ '!' => array('Image'),
+ '&' => array('SpecialCharacter'),
+ '*' => array('Emphasis'),
+ ':' => array('Url'),
+ '<' => array('UrlTag', 'EmailTag', 'Markup'),
+ '[' => array('Link'),
+ '_' => array('Emphasis'),
+ '`' => array('Code'),
+ '~' => array('Strikethrough'),
+ '\\' => array('EscapeSequence'),
+ );
+
+ # ~
+
+ protected $inlineMarkerList = '!*_&[:<`~\\';
+
+ #
+ # ~
+ #
+
+ public function line($text, $nonNestables = array())
+ {
+ return $this->elements($this->lineElements($text, $nonNestables));
+ }
+
+ protected function lineElements($text, $nonNestables = array())
+ {
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ $Elements = array();
+
+ $nonNestables = (empty($nonNestables)
+ ? array()
+ : array_combine($nonNestables, $nonNestables)
+ );
+
+ # $excerpt is based on the first occurrence of a marker
+
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
+ {
+ $marker = $excerpt[0];
+
+ $markerPosition = strlen($text) - strlen($excerpt);
+
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
+
+ foreach ($this->InlineTypes[$marker] as $inlineType)
+ {
+ # check to see if the current inline type is nestable in the current context
+
+ if (isset($nonNestables[$inlineType]))
+ {
+ continue;
+ }
+
+ $Inline = $this->{"inline$inlineType"}($Excerpt);
+
+ if ( ! isset($Inline))
+ {
+ continue;
+ }
+
+ # makes sure that the inline belongs to "our" marker
+
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
+ {
+ continue;
+ }
+
+ # sets a default inline position
+
+ if ( ! isset($Inline['position']))
+ {
+ $Inline['position'] = $markerPosition;
+ }
+
+ # cause the new element to 'inherit' our non nestables
+
+
+ $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
+ ? array_merge($Inline['element']['nonNestables'], $nonNestables)
+ : $nonNestables
+ ;
+
+ # the text that comes before the inline
+ $unmarkedText = substr($text, 0, $Inline['position']);
+
+ # compile the unmarked text
+ $InlineText = $this->inlineText($unmarkedText);
+ $Elements[] = $InlineText['element'];
+
+ # compile the inline
+ $Elements[] = $this->extractElement($Inline);
+
+ # remove the examined text
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+ continue 2;
+ }
+
+ # the marker does not belong to an inline
+
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+ $InlineText = $this->inlineText($unmarkedText);
+ $Elements[] = $InlineText['element'];
+
+ $text = substr($text, $markerPosition + 1);
+ }
+
+ $InlineText = $this->inlineText($text);
+ $Elements[] = $InlineText['element'];
+
+ foreach ($Elements as &$Element)
+ {
+ if ( ! isset($Element['autobreak']))
+ {
+ $Element['autobreak'] = false;
+ }
+ }
+
+ return $Elements;
+ }
+
+ #
+ # ~
+ #
+
+ protected function inlineText($text)
+ {
+ $Inline = array(
+ 'extent' => strlen($text),
+ 'element' => array(),
+ );
+
+ $Inline['element']['elements'] = self::pregReplaceElements(
+ $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
+ array(
+ array('name' => 'br'),
+ array('text' => "\n"),
+ ),
+ $text
+ );
+
+ return $Inline;
+ }
+
+ protected function inlineCode($Excerpt)
+ {
+ $marker = $Excerpt['text'][0];
+
+ if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
+ {
+ $text = $matches[2];
+ $text = preg_replace('/[ ]*+\n/', ' ', $text);
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmailTag($Excerpt)
+ {
+ $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
+
+ $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
+ . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
+
+ if (strpos($Excerpt['text'], '>') !== false
+ and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
+ ){
+ $url = $matches[1];
+
+ if ( ! isset($matches[2]))
+ {
+ $url = "mailto:$url";
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[1],
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmphasis($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ $marker = $Excerpt['text'][0];
+
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'strong';
+ }
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'em';
+ }
+ else
+ {
+ return;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => $emphasis,
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $matches[1],
+ 'destination' => 'elements',
+ )
+ ),
+ );
+ }
+
+ protected function inlineEscapeSequence($Excerpt)
+ {
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+ {
+ return array(
+ 'element' => array('rawHtml' => $Excerpt['text'][1]),
+ 'extent' => 2,
+ );
+ }
+ }
+
+ protected function inlineImage($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+ {
+ return;
+ }
+
+ $Excerpt['text']= substr($Excerpt['text'], 1);
+
+ $Link = $this->inlineLink($Excerpt);
+
+ if ($Link === null)
+ {
+ return;
+ }
+
+ $Inline = array(
+ 'extent' => $Link['extent'] + 1,
+ 'element' => array(
+ 'name' => 'img',
+ 'attributes' => array(
+ 'src' => $Link['element']['attributes']['href'],
+ 'alt' => $Link['element']['handler']['argument'],
+ ),
+ 'autobreak' => true,
+ ),
+ );
+
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+ unset($Inline['element']['attributes']['href']);
+
+ return $Inline;
+ }
+
+ protected function inlineLink($Excerpt)
+ {
+ $Element = array(
+ 'name' => 'a',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => null,
+ 'destination' => 'elements',
+ ),
+ 'nonNestables' => array('Url', 'Link'),
+ 'attributes' => array(
+ 'href' => null,
+ 'title' => null,
+ ),
+ );
+
+ $extent = 0;
+
+ $remainder = $Excerpt['text'];
+
+ if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
+ {
+ $Element['handler']['argument'] = $matches[1];
+
+ $extent += strlen($matches[0]);
+
+ $remainder = substr($remainder, $extent);
+ }
+ else
+ {
+ return;
+ }
+
+ if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches))
+ {
+ $Element['attributes']['href'] = $matches[1];
+
+ if (isset($matches[2]))
+ {
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+ }
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+ {
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
+ $definition = strtolower($definition);
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ $definition = strtolower($Element['handler']['argument']);
+ }
+
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
+ {
+ return;
+ }
+
+ $Definition = $this->DefinitionData['Reference'][$definition];
+
+ $Element['attributes']['href'] = $Definition['url'];
+ $Element['attributes']['title'] = $Definition['title'];
+ }
+
+ return array(
+ 'extent' => $extent,
+ 'element' => $Element,
+ );
+ }
+
+ protected function inlineMarkup($Excerpt)
+ {
+ if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'element' => array('rawHtml' => $matches[0]),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'element' => array('rawHtml' => $matches[0]),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'element' => array('rawHtml' => $matches[0]),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+ }
+
+ protected function inlineSpecialCharacter($Excerpt)
+ {
+ if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false
+ and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
+ ) {
+ return array(
+ 'element' => array('rawHtml' => '&' . $matches[1] . ';'),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ return;
+ }
+
+ protected function inlineStrikethrough($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'del',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $matches[1],
+ 'destination' => 'elements',
+ )
+ ),
+ );
+ }
+ }
+
+ protected function inlineUrl($Excerpt)
+ {
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+ {
+ return;
+ }
+
+ if (strpos($Excerpt['context'], 'http') !== false
+ and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
+ ) {
+ $url = $matches[0][0];
+
+ $Inline = array(
+ 'extent' => strlen($matches[0][0]),
+ 'position' => $matches[0][1],
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+
+ return $Inline;
+ }
+ }
+
+ protected function inlineUrlTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ # ~
+
+ protected function unmarkedText($text)
+ {
+ $Inline = $this->inlineText($text);
+ return $this->element($Inline['element']);
+ }
+
+ #
+ # Handlers
+ #
+
+ protected function handle(array $Element)
+ {
+ if (isset($Element['handler']))
+ {
+ if (!isset($Element['nonNestables']))
+ {
+ $Element['nonNestables'] = array();
+ }
+
+ if (is_string($Element['handler']))
+ {
+ $function = $Element['handler'];
+ $argument = $Element['text'];
+ unset($Element['text']);
+ $destination = 'rawHtml';
+ }
+ else
+ {
+ $function = $Element['handler']['function'];
+ $argument = $Element['handler']['argument'];
+ $destination = $Element['handler']['destination'];
+ }
+
+ $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
+
+ if ($destination === 'handler')
+ {
+ $Element = $this->handle($Element);
+ }
+
+ unset($Element['handler']);
+ }
+
+ return $Element;
+ }
+
+ protected function handleElementRecursive(array $Element)
+ {
+ return $this->elementApplyRecursive(array($this, 'handle'), $Element);
+ }
+
+ protected function handleElementsRecursive(array $Elements)
+ {
+ return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
+ }
+
+ protected function elementApplyRecursive($closure, array $Element)
+ {
+ $Element = call_user_func($closure, $Element);
+
+ if (isset($Element['elements']))
+ {
+ $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
+ }
+ elseif (isset($Element['element']))
+ {
+ $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
+ }
+
+ return $Element;
+ }
+
+ protected function elementApplyRecursiveDepthFirst($closure, array $Element)
+ {
+ if (isset($Element['elements']))
+ {
+ $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
+ }
+ elseif (isset($Element['element']))
+ {
+ $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
+ }
+
+ $Element = call_user_func($closure, $Element);
+
+ return $Element;
+ }
+
+ protected function elementsApplyRecursive($closure, array $Elements)
+ {
+ foreach ($Elements as &$Element)
+ {
+ $Element = $this->elementApplyRecursive($closure, $Element);
+ }
+
+ return $Elements;
+ }
+
+ protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
+ {
+ foreach ($Elements as &$Element)
+ {
+ $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
+ }
+
+ return $Elements;
+ }
+
+ protected function element(array $Element)
+ {
+ if ($this->safeMode)
+ {
+ $Element = $this->sanitiseElement($Element);
+ }
+
+ # identity map if element has no handler
+ $Element = $this->handle($Element);
+
+ $hasName = isset($Element['name']);
+
+ $markup = '';
+
+ if ($hasName)
+ {
+ $markup .= '<' . $Element['name'];
+
+ if (isset($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $name => $value)
+ {
+ if ($value === null)
+ {
+ continue;
+ }
+
+ $markup .= " $name=\"".self::escape($value).'"';
+ }
+ }
+ }
+
+ $permitRawHtml = false;
+
+ if (isset($Element['text']))
+ {
+ $text = $Element['text'];
+ }
+ // very strongly consider an alternative if you're writing an
+ // extension
+ elseif (isset($Element['rawHtml']))
+ {
+ $text = $Element['rawHtml'];
+
+ $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
+ $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
+ }
+
+ $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
+
+ if ($hasContent)
+ {
+ $markup .= $hasName ? '>' : '';
+
+ if (isset($Element['elements']))
+ {
+ $markup .= $this->elements($Element['elements']);
+ }
+ elseif (isset($Element['element']))
+ {
+ $markup .= $this->element($Element['element']);
+ }
+ else
+ {
+ if (!$permitRawHtml)
+ {
+ $markup .= self::escape($text, true);
+ }
+ else
+ {
+ $markup .= $text;
+ }
+ }
+
+ $markup .= $hasName ? '</' . $Element['name'] . '>' : '';
+ }
+ elseif ($hasName)
+ {
+ $markup .= ' />';
+ }
+
+ return $markup;
+ }
+
+ protected function elements(array $Elements)
+ {
+ $markup = '';
+
+ $autoBreak = true;
+
+ foreach ($Elements as $Element)
+ {
+ if (empty($Element))
+ {
+ continue;
+ }
+
+ $autoBreakNext = (isset($Element['autobreak'])
+ ? $Element['autobreak'] : isset($Element['name'])
+ );
+ // (autobreak === false) covers both sides of an element
+ $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
+
+ $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
+ $autoBreak = $autoBreakNext;
+ }
+
+ $markup .= $autoBreak ? "\n" : '';
+
+ return $markup;
+ }
+
+ # ~
+
+ protected function li($lines)
+ {
+ $Elements = $this->linesElements($lines);
+
+ if ( ! in_array('', $lines)
+ and isset($Elements[0]) and isset($Elements[0]['name'])
+ and $Elements[0]['name'] === 'p'
+ ) {
+ unset($Elements[0]['name']);
+ }
+
+ return $Elements;
+ }
+
+ #
+ # AST Convenience
+ #
+
+ /**
+ * Replace occurrences $regexp with $Elements in $text. Return an array of
+ * elements representing the replacement.
+ */
+ protected static function pregReplaceElements($regexp, $Elements, $text)
+ {
+ $newElements = array();
+
+ while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE))
+ {
+ $offset = $matches[0][1];
+ $before = substr($text, 0, $offset);
+ $after = substr($text, $offset + strlen($matches[0][0]));
+
+ $newElements[] = array('text' => $before);
+
+ foreach ($Elements as $Element)
+ {
+ $newElements[] = $Element;
+ }
+
+ $text = $after;
+ }
+
+ $newElements[] = array('text' => $text);
+
+ return $newElements;
+ }
+
+ #
+ # Deprecated Methods
+ #
+
+ function parse($text)
+ {
+ $markup = $this->text($text);
+
+ return $markup;
+ }
+
+ protected function sanitiseElement(array $Element)
+ {
+ static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
+ static $safeUrlNameToAtt = array(
+ 'a' => 'href',
+ 'img' => 'src',
+ );
+
+ if ( ! isset($Element['name']))
+ {
+ unset($Element['attributes']);
+ return $Element;
+ }
+
+ if (isset($safeUrlNameToAtt[$Element['name']]))
+ {
+ $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
+ }
+
+ if ( ! empty($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $att => $val)
+ {
+ # filter out badly parsed attribute
+ if ( ! preg_match($goodAttribute, $att))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ # dump onevent attribute
+ elseif (self::striAtStart($att, 'on'))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ }
+ }
+
+ return $Element;
+ }
+
+ protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
+ {
+ foreach ($this->safeLinksWhitelist as $scheme)
+ {
+ if (self::striAtStart($Element['attributes'][$attribute], $scheme))
+ {
+ return $Element;
+ }
+ }
+
+ $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
+
+ return $Element;
+ }
+
+ #
+ # Static Methods
+ #
+
+ protected static function escape($text, $allowQuotes = false)
+ {
+ return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
+ }
+
+ protected static function striAtStart($string, $needle)
+ {
+ $len = strlen($needle);
+
+ if ($len > strlen($string))
+ {
+ return false;
+ }
+ else
+ {
+ return strtolower(substr($string, 0, $len)) === strtolower($needle);
+ }
+ }
+
+ static function instance($name = 'default')
+ {
+ if (isset(self::$instances[$name]))
+ {
+ return self::$instances[$name];
+ }
+
+ $instance = new static();
+
+ self::$instances[$name] = $instance;
+
+ return $instance;
+ }
+
+ private static $instances = array();
+
+ #
+ # Fields
+ #
+
+ protected $DefinitionData;
+
+ #
+ # Read-Only
+
+ protected $specialCharacters = array(
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
+ );
+
+ protected $StrongRegex = array(
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
+ );
+
+ protected $EmRegex = array(
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+ );
+
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
+
+ protected $voidElements = array(
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
+ );
+
+ protected $textLevelElements = array(
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
+ 'q', 'rt', 'ins', 'font', 'strong',
+ 's', 'tt', 'kbd', 'mark',
+ 'u', 'xm', 'sub', 'nobr',
+ 'sup', 'ruby',
+ 'var', 'span',
+ 'wbr', 'time',
+ );
+} \ No newline at end of file
diff --git a/includes/session.php b/includes/session.php
index e9f9f30..4bda760 100644
--- a/includes/session.php
+++ b/includes/session.php
@@ -29,12 +29,28 @@ if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users")) mkdir($_SERVER[
if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-favorites.json")) file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-favorites.json", "[]");
if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-library.json")) file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-library.json", "[]");
if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-history.json")) file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-history.json", "[]");
+if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-privacy.json")) file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-privacy.json", json_encode([
+ "profile" => 2,
+ "library" => 0,
+ "history" => 0,
+ "favorites" => 0,
+ "custom" => 1,
+ "listen" => 0
+]));
+if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-profileSettings.json")) file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-profileSettings.json", json_encode([
+ "nsfw" => false,
+ "description" => "",
+ "banner" => ""
+]));
+file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-profile.json", json_encode($_PROFILE));
$albums = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/assets/content/albums.json"), true);
$songs = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/assets/content/songs.json"), true);
$favorites = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-favorites.json"), true);
$library = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-library.json"), true);
$history = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-history.json"), true);
+$privacy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-privacy.json"), true);
+$profile = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $_PROFILE["id"] . "-profileSettings.json"), true);
$albums = array_map(function ($i) {
$i["artist"] = str_replace(";", ", ", $i["artist"]);
@@ -111,7 +127,7 @@ function displayList($list, $hasAlbum = false) { global $albums; global $favorit
}
function getInfo(id) {
- window.parent.openModal((window.parent.songs[id]?.artist ?? "Unknown artist") + " - " + (window.parent.songs[id]?.title ?? "Unknown song"), "info.php?i=" + id);
+ window.parent.openModal((window.parent.songs[id]?.artist ?? "Unknown artist") + " - " + (window.parent.songs[id]?.title ?? "Unknown song"), "info.php?i=" + id, true);
}
function enqueue(id) {
diff --git a/oauth/.DS_Store b/oauth/.DS_Store
index b1ba7bb..1333ff9 100644
--- a/oauth/.DS_Store
+++ b/oauth/.DS_Store
Binary files differ
diff --git a/oauth/callback-native/index.php b/oauth/callback-native/index.php
index c5d0b44..c289957 100644
--- a/oauth/callback-native/index.php
+++ b/oauth/callback-native/index.php
@@ -39,7 +39,8 @@ if (isset($result["access_token"])) {
$result = json_decode($result, true);
if (!in_array($result["id"], $app["allowed"])) {
- die();
+ header("HTTP/1.1 403 Forbidden");
+ die("Not allowed to log in to this application. This will be reported.");
}
if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens")) mkdir($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens");
diff --git a/oauth/callback/index.php b/oauth/callback/index.php
index d65bced..bbb3322 100644
--- a/oauth/callback/index.php
+++ b/oauth/callback/index.php
@@ -39,7 +39,8 @@ if (isset($result["access_token"])) {
$result = json_decode($result, true);
if (!in_array($result["id"], $app["allowed"])) {
- die();
+ header("HTTP/1.1 403 Forbidden");
+ die("Not allowed to log in to this application. This will be reported.");
}
if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens")) mkdir($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens");
diff --git a/profile/index.php b/profile/index.php
new file mode 100644
index 0000000..1fb6ac3
--- /dev/null
+++ b/profile/index.php
@@ -0,0 +1,418 @@
+<?php header("X-Frame-Options: SAMEORIGIN");
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/Parsedown.php"; $Parsedown = new Parsedown();
+$albums = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/assets/content/albums.json"), true);
+$songs = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/assets/content/songs.json"), true);
+
+function timeAgo($time, $french = false, $isDifference = false, $long = true, $showTense = true): string {
+ $lengths = array("60", "60", "24", "7", "4.35", "12", "100");
+
+ if ($long) {
+ $periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"];
+ } else {
+ $periods = ["sec", "min", "hr", "d", "wk", "mo", "y", "ages"];
+ }
+
+ if ($isDifference) {
+ $difference = $time;
+ } else {
+ if (!is_numeric($time)) {
+ $time = strtotime($time);
+ }
+
+ $now = time();
+ $difference = $now - $time;
+ }
+
+ if ($difference <= 10 && $difference >= 0) {
+ return $tense = "now";
+ } elseif ($difference > 0) {
+ $tense = "ago";
+ } else {
+ $tense = "later";
+ }
+
+ for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
+ $difference /= $lengths[$j];
+ }
+
+ $difference = round($difference);
+
+ $period = $periods[$j];
+
+ if ($showTense) {
+ if ($long) {
+ return "{$difference} {$period}" . ($difference > 1 ? "s" : "") . " {$tense}";
+ } else {
+ return "{$difference} {$period} {$tense}";
+ }
+ } else {
+ if ($long) {
+ return "{$difference} {$period}" . ($difference > 1 ? "s" : "");
+ } else {
+ return "{$difference} {$period}";
+ }
+ }
+}
+
+global $_PROFILE;
+$_PROFILE = null;
+
+if (isset($_COOKIE["WAVY_SESSION_TOKEN"])) {
+ if (!str_contains($_COOKIE["WAVY_SESSION_TOKEN"], ".") && !str_contains($_COOKIE["WAVY_SESSION_TOKEN"], "/")) {
+ if (str_starts_with($_COOKIE["WAVY_SESSION_TOKEN"], "wv_")) {
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . $_COOKIE["WAVY_SESSION_TOKEN"])) {
+ $_PROFILE = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . $_COOKIE["WAVY_SESSION_TOKEN"]), true);
+ }
+ }
+ }
+}
+
+$available = false;
+global $userPrivacy;
+
+$userProfile = [];
+$userFavorites = [];
+$userHistory = [];
+$userLibrary = [];
+$userSettings = [];
+
+if (count($_GET) > 0 && str_starts_with(array_keys($_GET)[0], "/")) {
+ $hasID = true;
+ $selectedId = substr(array_keys($_GET)[0], 1);
+
+ if (preg_match("/[^a-f0-9-]/m", $selectedId) == 0) {
+ if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $selectedId . "-privacy.json")) {
+ $userPrivacy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $selectedId . "-privacy.json"), true);
+ $userProfile = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $selectedId . "-profile.json"), true);
+
+ if ($userPrivacy["profile"] >= 1 && isset($_PROFILE) || $userPrivacy["profile"] === 2 || $_PROFILE["id"] === $userProfile["id"]) {
+ $available = true;
+
+ $userFavorites = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $selectedId . "-favorites.json"), true);
+ $userHistory = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $selectedId . "-history.json"), true);
+ $userLibrary = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $selectedId . "-library.json"), true);
+ $userSettings = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/users/" . $selectedId . "-profileSettings.json"), true);
+
+ $userLibrary = array_values(array_filter($userLibrary, function ($i) {
+ global $albums;
+ return isset($albums[$i]);
+ }));
+ $userHistory = array_values(array_filter($userHistory, function ($i) {
+ global $songs;
+ return isset($songs[$i["item"]]);
+ }));
+ $userFavorites = array_values(array_filter($userFavorites, function ($i) {
+ global $songs;
+ return isset($songs[$i]);
+ }));
+ }
+ }
+ }
+} else {
+ $hasID = false;
+}
+
+if (!$available && $hasID) {
+ header("HTTP/1.1 404 Not Found");
+}
+
+function allowed(string $item): bool {
+ global $userPrivacy; global $userProfile; global $_PROFILE;
+ return $userPrivacy[$item] >= 1 && isset($_PROFILE) || $userPrivacy[$item] === 2 || $_PROFILE["id"] === $userProfile["id"];
+}
+
+?>
+<!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><?= $available ? $userProfile['name'] . " — Mist" : "Mist" ?></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 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)">
+ <style>
+ @media (min-width: 768px) {
+ #copyright-separator-mobile {
+ display: none;
+ }
+ }
+
+ @media (max-width: 767px) {
+ #copyright-separator-desktop {
+ display: none;
+ }
+ }
+ </style>
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:site" content="@equestriadev" />
+ <meta name="twitter:title" content="<?= $available ? $userProfile['name'] . " (@" . $userProfile["login"] . ") on Mist" : "Mist" ?>" />
+ <meta name="twitter:description" content="<?= $available ? "View " . $userProfile['name'] . "'s profile on Mist, including their favorite songs, listening history, and album library." : "Mist" ?>" />
+ <meta name="twitter:image" content="<?= $available ? "https://account.equestria.dev/hub/api/rest/avatar/" . $userProfile["id"] . "?dpr=2&size=64" : '' ?>" />
+ <meta name="description" content="<?= $available ? "View " . $userProfile['name'] . "'s profile on Mist, including their favorite songs, listening history, and album library." : "Mist" ?>">
+ <meta property="og:type" content="profile" />
+ <meta property="og:title" content="<?= $available ? $userProfile['name'] . " (@" . $userProfile["login"] . ") on Mist" : "Mist" ?>" />
+ <meta property="og:description" content="<?= $available ? "View " . $userProfile['name'] . "'s profile on Mist, including their favorite songs, listening history, and album library." : "Mist" ?>" />
+ <meta property="og:url" content="https://html.sammy-codes.com/" />
+ <meta property="og:image" content="<?= $available ? "https://account.equestria.dev/hub/api/rest/avatar/" . $userProfile["id"] . "?dpr=2&size=64" : '' ?>" />
+</head>
+
+<body>
+ <div id="top-bar" style="position: relative; padding: 10px 20px; text-align: right; height: 52px !important; z-index: 99;">
+ <?php if (isset($_PROFILE)): ?>
+ <a target="_blank" href="https://account.equestria.dev/hub/users/<?= $_PROFILE["id"] ?>">
+ <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;">
+ </a>
+ <?php else: ?>
+ <a href="/oauth/init">Sign in</a>
+ <?php endif; ?>
+ </div>
+
+ <?php if (allowed("custom") && $userSettings["nsfw"]): ?>
+ <div class="modal" id="nsfw" style="backdrop-filter: blur(100px);-webkit-backdrop-filter: blur(100px);" data-bs-backdrop="static" data-bs-keyboard="false">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-body">
+ <div style="text-align: center;">
+ <img src="/icons/adult.svg" style="width: 64px; height: 64px; margin-bottom: 10px;">
+ <h3>This profile contains adult material that you might not want to view.</h3>
+ <p>The owner of this profile has marked it as not-safe-for-work, indicating that the content on it might not be pleasant for everything. This could include sexually explicit content, violent acts, or ethically controversial imagery.</p>
+ <p>Do you want to continue viewing this profile?</p>
+ <a data-bs-dismiss="modal" class="btn btn-primary">Continue</a>
+ <a href="/profile" class="btn btn-outline-primary">Cancel</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ document.getElementById("nsfw").addEventListener("shown.bs.modal", () => {
+ document.getElementById("nsfw").classList.add("fade");
+ });
+
+ (new bootstrap.Modal(document.getElementById("nsfw"))).show();
+ </script>
+ <?php endif; ?>
+
+ <?php if ($available): ?>
+ <?php if (allowed("custom") && trim($userSettings["banner"]) !== ""): ?>
+ <div id="banner" style="background-size: cover; background-position: center; background-image: url(&quot;<?= str_replace('"', '', $userSettings["banner"]) ?>&quot;); background-color: #eee; height: 256px; margin-top: -52px; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px;">
+ <div style="background-color: rgba(255, 255, 255, .25); height: 100%; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px;"></div>
+ <?php else: ?>
+ <div id="banner" style="background-size: cover; background-position: center; background-image: url('https://account.equestria.dev/hub/api/rest/avatar/<?= $userProfile["id"] ?>?dpr=2&size=32'); background-color: #eee; height: 256px; margin-top: -52px; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px;">
+ <div style="background-color: rgba(255, 255, 255, .25); height: 100%; backdrop-filter: blur(100px); -webkit-backdrop-filter: blur(50px); border-bottom-left-radius: 20px; border-bottom-right-radius: 20px;"></div>
+ <?php endif; ?>
+ <div style="text-align: center; margin-top: -64px; position: relative; z-index: 99;">
+ <img alt="" src="https://account.equestria.dev/hub/api/rest/avatar/<?= $userProfile["id"] ?>?dpr=2&size=64" style="border-radius: 999px; width: 128px; height: 128px; box-shadow: 0px 0px 20px rgba(0, 0, 0, .25);">
+ <h3 style="margin-top: 10px;"><?= $userProfile['name'] ?></h3>
+ <h5 class="text-muted">@<?= $userProfile["login"] ?></h5>
+ </div>
+ </div>
+
+ <div class="container" style="margin-top: 159px;">
+ <?php if (allowed("custom") && trim($userSettings["description"]) !== ""): ?>
+ <?= $Parsedown->text($userSettings["description"]) ?>
+ <?php endif; ?>
+
+ <?php if (allowed("history")): ?>
+ <p class="text-muted">
+ <?php if (isset($userHistory[0])): ?>
+ Last listened <?= timeAgo($userHistory[count($userHistory) - 1]["date"]) ?>: <?= $songs[$userHistory[count($userHistory) - 1]["item"]]["artist"] ?> - <?= $songs[$userHistory[count($userHistory) - 1]["item"]]["title"] ?>
+ <?php else: ?>
+ Never listened to anything
+ <?php endif; ?>
+ </p>
+
+ <?php endif; echo("<hr>"); if (allowed("history")): ?>
+
+ <h3 style="margin-bottom: 15px;">Listening history</h3>
+ <div class="list-group">
+ <?php usort($userHistory, function ($a, $b) {
+ return strtotime($b["date"]) - strtotime($a["date"]);
+ }); foreach (array_slice(array_values($userHistory), 0, 7) as $history): $song = $songs[$history["item"]]; ?>
+ <div class="list-group-item" style="display: grid; grid-template-columns: 64px 1fr; grid-gap: 15px;">
+ <div style="display: flex; align-items: center;">
+ <img alt="" src="/albumart.php?i=<?= $history["item"] ?>" style="width: 64px; height: 64px; background-color: #eee; border-radius: 5px;">
+ </div>
+ <div style="display: flex; align-items: center;">
+ <div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $song["title"] ?></div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;" class="text-muted"><?= $song["artist"] ?></div>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+
+ <hr>
+ <?php endif; ?>
+
+ <?php if (allowed("favorites")): ?>
+ <h3 style="margin-bottom: 15px;">Favorite songs</h3>
+ <div class="list-group">
+ <?php foreach (array_slice(array_reverse($userFavorites), 0, 7) as $item): $song = $songs[$item]; ?>
+ <div class="list-group-item" style="display: grid; grid-template-columns: 64px 1fr; grid-gap: 15px;">
+ <div style="display: flex; align-items: center;">
+ <img alt="" src="/albumart.php?i=<?= $item ?>" style="width: 64px; height: 64px; background-color: #eee; border-radius: 5px;">
+ </div>
+ <div style="display: flex; align-items: center;">
+ <div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $song["title"] ?></div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;" class="text-muted"><?= $song["artist"] ?></div>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+ <?php if (count($userFavorites) > 15): ?>
+ <div class="modal fade" id="favorites">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title">Favorite songs</h4>
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+ </div>
+
+ <div class="modal-body">
+ <div class="list-group">
+ <?php foreach ($userFavorites as $item): $song = $songs[$item]; ?>
+ <div class="list-group-item" style="display: grid; grid-template-columns: 64px 1fr; grid-gap: 15px;">
+ <div style="display: flex; align-items: center;">
+ <img alt="" src="/albumart.php?i=<?= $item ?>" style="width: 64px; height: 64px; background-color: #eee; border-radius: 5px;">
+ </div>
+ <div style="display: flex; align-items: center;">
+ <div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $song["title"] ?></div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;" class="text-muted"><?= $song["artist"] ?></div>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="all-favorites" style="margin-top: 1rem;"><a href="#" data-bs-toggle="modal" data-bs-target="#favorites" id="all-favorites-link">View all favorite songs (<?= count($userFavorites) ?>)</a></div>
+ <?php endif; ?>
+
+ <hr>
+ <?php endif; ?>
+
+ <?php if (allowed("library")): ?>
+ <h3 style="margin-bottom: 15px;">Albums in library</h3>
+ <div class="list-group">
+ <?php foreach (array_slice(array_reverse($userLibrary), 0, 7) as $item): $album = $albums[$item]; ?>
+ <div class="list-group-item" style="display: grid; grid-template-columns: 64px 1fr; grid-gap: 15px;">
+ <div style="display: flex; align-items: center;">
+ <img alt="" src="/albumart.php?i=<?= $item ?>" style="width: 64px; height: 64px; background-color: #eee; border-radius: 5px;">
+ </div>
+ <div style="display: flex; align-items: center;">
+ <div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $album["title"] ?></div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;" class="text-muted"><?= $album["artist"] ?></div>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+ <?php if (count($userLibrary) > 15): ?>
+ <div class="modal fade" id="albums">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title">Albums in library</h4>
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+ </div>
+
+ <div class="modal-body">
+ <div class="list-group">
+ <?php foreach ($userLibrary as $item): $album = $albums[$item]; ?>
+ <div class="list-group-item" style="display: grid; grid-template-columns: 64px 1fr; grid-gap: 15px;">
+ <div style="display: flex; align-items: center;">
+ <img alt="" src="/albumart.php?i=<?= $item ?>" style="width: 64px; height: 64px; background-color: #eee; border-radius: 5px;">
+ </div>
+ <div style="display: flex; align-items: center;">
+ <div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;"><?= $album["title"] ?></div>
+ <div style="white-space: nowrap; overflow: hidden !important; text-overflow: ellipsis;" class="text-muted"><?= $album["artist"] ?></div>
+ </div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="all-favorites" style="margin-top: 1rem;"><a href="#" data-bs-toggle="modal" data-bs-target="#albums" id="all-albums-link">View all albums in library (<?= count($userLibrary) ?>)</a></div>
+ <?php endif; ?>
+
+ <hr>
+ <?php endif; ?>
+
+ <div class="text-muted">
+ <img class="icon" src="/icons/logo-transparent.svg" style="vertical-align: middle; filter: grayscale(1) invert(1); width: 32px; height: 32px;" alt="">
+ <span style="vertical-align: middle;">Powered by <a class="link-secondary" href="https://source.equestria.dev/equestria.dev/mist" target="_blank">Mist</a> (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>
+ </div>
+
+ <br><br>
+ <?php else: ?>
+ <?php if ($hasID): ?>
+ <div class="container">
+ <br><br>
+ <h2>Searched far and wide... nothing!</h2>
+ <p>No Mist profile associated with the provided URL could be found, or you don't have permission to view the requested profile. Make sure the URL is valid and contact the profile owner if you don't have permission to view it.</p>
+ <?php if (!isset($_PROFILE)): ?>
+ <p>Note that you are currently logged out. If you think logged-in Mist users can access this profile, you can <a href="/oauth/init">sign in</a> and try again.</p>
+ <?php endif; ?>
+ <details style="margin-bottom: 1rem !important;">
+ <summary>How do I update the permissions for my profile?</summary>
+
+ <div class="list-group" style="margin-top: 10px;">
+ <div class="list-group-item">
+ <ul style="margin-top: 1rem !important;">
+ <li>Open Mist</li>
+ <li>Go to "Settings"</li>
+ <li>Scroll down to "Privacy"</li>
+ <li>On "Who can see your Mist profile?", select "All Mist users" or "Everyone"</li>
+ <li>Done!</li>
+ </ul>
+ </div>
+ </div>
+ </details>
+
+ <?php if (isset($_PROFILE)): ?><a href="/app/">Go back to Mist</a><?php endif; ?>
+ </div>
+ <?php else: ?>
+ <div class="container">
+ <br><br>
+ <h2>Welcome to the Mist public profile browser!</h2>
+ <p>If you see this page after following a link to a Mist profile, something must have gone wrong. Try clicking on the link again or contact the person who has provided the link for more information. If you are interested in joining Mist, you can <a href="mailto:raindrops@equestria.dev" target="_blank">get in touch with us</a>, and we will see what we can do for you.</p>
+ <?php if (isset($_PROFILE)): ?>
+ <p>Since you are logged in, you might want to start by <a href="/profile/?/<?= $_PROFILE["id"] ?>">viewing your own profile</a>. You can ask other people for their profile URL to view what is on their profile (depending on what they have decided to show); or you can <a href="/app/">open Mist</a> and change settings for your profile.</p>
+ <a href="/app/">Go back to Mist</a>
+ <?php endif; ?>
+ </div>
+ <?php endif; ?>
+
+ <br><br>
+ <?php endif; ?>
+</body>
+
+</html> \ No newline at end of file
diff --git a/version b/version
index 308b6fa..9dbb0c0 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-1.6.2 \ No newline at end of file
+1.7.0 \ No newline at end of file