def index():
users = [row["username"] for row in query_db("select username from users order by username asc")]
songs = Song.get_latest(50)
- song_list = render_template("song-list.html", songs=songs)
- return render_template("index.html", users=users, song_list=song_list)
+ return render_template("index.html", users=users, songs=songs)
@app.get("/signup")
def signup_get():
playlists=plist_data,
songs=songs,
user_has_pfp=(get_user_images_path(profile_userid)/"pfp.jpg").exists(),
- song_list=render_template("song-list.html", songs=songs, is_profile_song_list=True))
+ is_profile_song_list=True)
@app.post("/edit-profile")
def edit_profile():
yt_url = request.form["song-url"] if "song-url" in request.form else None
title = request.form["title"]
description = request.form["description"]
- tags = [t.strip() for t in request.form["tags"].split(",")]
- collaborators = [c.strip() for c in request.form["collabs"].split(",")]
+ tags = [t.strip() for t in request.form["tags"].split(",") if t]
+ collaborators = [c.strip() for c in request.form["collabs"].split(",") if c]
# Make sure song exists and the logged-in user owns it
song_data = query_db("select * from songs where songid = ?", [songid], one=True)
yt_url = request.form["song-url"] if "song-url" in request.form else None
title = request.form["title"]
description = request.form["description"]
- tags = [t.strip() for t in request.form["tags"].split(",")]
- collaborators = [c.strip() for c in request.form["collabs"].split(",")]
+ tags = [t.strip() for t in request.form["tags"].split(",") if t]
+ collaborators = [c.strip() for c in request.form["collabs"].split(",") if c]
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
passed = convert_song(tmp_file, file, yt_url)
user_data = query_db("select * from users where userid = ?", [userid], one=True)
return render_template(
"song.html",
- song_list=render_template("song-list.html", songs=[song]),
+ songs=[song],
song=song,
bgcolor=user_data["bgcolor"],
fgcolor=user_data["fgcolor"],
else:
songs = []
- return render_template(
- "songs-by-tag.html",
- user=user,
- tag=tag,
- song_list=render_template("song-list.html", songs=songs))
+ return render_template("songs-by-tag.html", user=user, tag=tag, songs=songs)
@app.route("/comment", methods=["GET", "POST"])
def comment():
bgcolor=plist_data["bgcolor"],
fgcolor=plist_data["fgcolor"],
accolor=plist_data["accolor"],
- songs=songs,
- song_list=render_template("song-list.html", songs=songs))
+ songs=songs)
def flash_and_log(msg, category=None):
flash(msg, category)
tags, collabs = cls._get_info_for_songs(songs_data)
songs = []
for sd in songs_data:
- song_tags = [t["tag"] for t in tags[sd["songid"]]]
- song_collabs = [c["name"] for c in collabs[sd["songid"]]]
+ song_tags = [t["tag"] for t in tags[sd["songid"]] if t["tag"]]
+ song_collabs = [c["name"] for c in collabs[sd["songid"]] if c["name"]]
created = datetime.fromisoformat(sd["created"]).astimezone().strftime("%Y-%m-%d")
user_has_pfp = (get_user_images_path(sd["userid"])/"pfp.jpg").exists()
songs.append(cls(sd["songid"], sd["userid"], sd["username"], sd["title"], sanitize_user_text(sd["description"]), created, song_tags, song_collabs, user_has_pfp))
--- /dev/null
+document.addEventListener("DOMContentLoaded", (e) => {
+
+ // Handle link clicks with AJAX
+ document.querySelectorAll("a").forEach((anchor) => {
+ anchor.removeEventListener("click", onLinkClick);
+ anchor.addEventListener("click", onLinkClick);
+ });
+
+ // Handle form submissions with AJAX
+ document.querySelectorAll("form").forEach((form) => {
+ form.removeEventListener("submit", onFormSubmit);
+ form.addEventListener("submit", onFormSubmit);
+ });
+
+ // Update colors
+ var mainDiv = document.getElementById("main");
+ var rootStyle = document.documentElement.style;
+ rootStyle.setProperty("--yellow", mainDiv.dataset.bgcolor);
+ rootStyle.setProperty("--black", mainDiv.dataset.fgcolor);
+ rootStyle.setProperty("--purple", mainDiv.dataset.accolor);
+
+ updateImageColors();
+
+ // Update navbar based on logged-in status
+ var username = mainDiv.dataset.username;
+ var loggedIn = username ? true : false;
+ document.querySelectorAll(".nav-logged-in").forEach((e) => {e.hidden = !loggedIn;});
+ document.querySelectorAll(".nav-logged-out").forEach((e) => {e.hidden = loggedIn;});
+ if (loggedIn) {
+ document.getElementById("logged-in-status").innerText = `Signed in as ${username}`;
+ }
+
+ // Update activity indicator status
+ checkForNewActivity();
+});
+
+function onLinkClick(event) {
+ var targetUrl = new URL(event.currentTarget.href);
+ if (urlIsOnSameSite(targetUrl)) {
+ event.preventDefault();
+ event.stopPropagation();
+ fetch(targetUrl, {redirect: "follow"}).then(handleAjaxResponse);
+ }
+}
+
+function onFormSubmit(event) {
+ var targetUrl = new URL(event.target.action);
+ if (urlIsOnSameSite(targetUrl)) {
+ event.preventDefault();
+ event.stopPropagation();
+ var formData = new FormData(event.target);
+ fetch(targetUrl, {redirect: "follow", body: formData, method: event.target.method})
+ .then(handleAjaxResponse);
+ }
+}
+
+function urlIsOnSameSite(targetUrl) {
+ var currentUrl = new URL(window.location.href);
+ return targetUrl.origin === currentUrl.origin;
+}
+
+async function handleAjaxResponse(response) {
+ // Update URL in browser window, minus request-type field
+ var url = new URL(response.url);
+ url.searchParams.delete("request-type");
+
+ // Get page content from XML response
+ var text = await response.text();
+ window.history.pushState(text, "", url);
+
+ updatePageState(text);
+}
+
+// Populate page state from history stack when user navigates back
+window.addEventListener("popstate", (event) => updatePageState(event.state));
+
+function updatePageState(data) {
+ // Replace the contents of the current page with those from data
+
+ if (!data) {
+ fetch(window.location.href, {redirect: "follow"}).then(handleAjaxResponse);
+ return;
+ }
+ var parser = new DOMParser();
+ data = parser.parseFromString(data, "text/html");
+
+ // Update main body content
+ var newMainDiv = data.getElementById("main");
+ var oldMainDiv = document.getElementById("main");
+ document.body.replaceChild(newMainDiv, oldMainDiv);
+
+ // Update flashed messages
+ var newFlashes = data.getElementById("flashes-container");
+ var oldFlashes = document.getElementById("flashes-container");
+ oldFlashes.parentElement.replaceChild(newFlashes, oldFlashes);
+
+ // Update page title
+ document.title = data.title;
+
+ // Load inline scripts (DOMParser disables these by default)
+ var scripts = document.getElementById("main").getElementsByTagName("script");
+ for (const script of scripts) {
+ var newScript = document.createElement("script");
+ newScript.type = script.type;
+ newScript.text = script.text;
+ script.parentElement.replaceChild(newScript, script);
+ }
+
+ // Delete old color picker (will be recreated on DOMContentLoaded)
+ document.getElementById("clr-picker").remove();
+
+ // Trigger event to signal new page has loaded
+ var event = new Event("DOMContentLoaded");
+ document.dispatchEvent(event);
+}
+
+async function checkForNewActivity() {
+ // Query the server to see if the user has new activity
+
+ // Only check for activity if user is logged in
+ var mainDiv = document.getElementById("main");
+ var username = mainDiv.dataset.username;
+ if (!username) {
+ return;
+ }
+
+ // Logged in - make the activity status request
+ const indicator = document.getElementById("activity-indicator")
+ const response = await fetch("/new-activity");
+ if (!response.ok) {
+ console.log(`Failed to get activity: ${response.status}`);
+ }
+ const json = await response.json();
+ indicator.hidden = !json.new_activity;
+}
+
+// Check for new activity every 10s
+setInterval(checkForNewActivity, 10000);
+
+function customImage(element) {
+ // Customize an image by performing a palette swap on the .gif
+ // file. The source element must contain a data-img-b64 attribute
+ // containing the base64 representation of a .gif file. The byte
+ // indexes match .gifs from Aseprite, and may not work for all
+ // .gif files.
+
+ var style = window.getComputedStyle(document.body);
+ var bgcolor = style.getPropertyValue("--yellow");
+ var accolor = style.getPropertyValue("--purple");
+
+ // Convert base64 string to Uint8Array so we can modify it
+ var data = atob(element.dataset.imgB64);
+ var bytes = Uint8Array.from(data, c => c.charCodeAt(0));
+
+ // Replace background color palette bytes in gif file
+ bytes[16] = parseInt(bgcolor.substring(1, 3), 16);
+ bytes[17] = parseInt(bgcolor.substring(3, 5), 16);
+ bytes[18] = parseInt(bgcolor.substring(5, 7), 16);
+
+ // Replace foreground color palette bytes in gif file
+ bytes[19] = parseInt(accolor.substring(1, 3), 16);
+ bytes[20] = parseInt(accolor.substring(3, 5), 16);
+ bytes[21] = parseInt(accolor.substring(5, 7), 16);
+
+ // Convert Uint8Array back to base64 so we can use it in a src string
+ data = btoa(String.fromCharCode(...bytes));
+
+ // Embed base64 in a data string that can be used as an img src.
+ return `data:image/gif;base64, ${data}`;
+}
+
+function updateImageColors() {
+ // Perform a palette swap on all gifs based on current page colors
+ document.querySelectorAll(".img-data").forEach(e => {
+ document.querySelectorAll(`.${e.id}`).forEach(t => {
+ t.src = customImage(e);
+ });
+ });
+}
+
<html>
<head>
<title>{% block title %}{% endblock %}</title>
- <link rel="stylesheet" href="/static/styles.css"/>
- <link rel="icon" type="image/x-icon" href="/static/lsp_notes.png"/>
- <script src="/static/player.js"></script>
+ <link rel="stylesheet" href="/static/styles.css?v=1"/>
+ <link rel="icon" type="image/x-icon" href="/static/lsp_notes.png?v=1"/>
+ <script src="/static/player.js?v=1"></script>
+ <script src="/static/nav.js?v=1"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Include coloris library for color picker -->
<script src="/static/coloris.min.js"></script>
<!-- Page-specific head fields -->
- {% block head %}
- {% endblock %}
- <script>
- document.addEventListener("DOMContentLoaded", (e) => {
-
- // Handle link clicks with AJAX
- document.querySelectorAll("a").forEach((anchor) => {
- anchor.removeEventListener("click", onLinkClick);
- anchor.addEventListener("click", onLinkClick);
- });
-
- // Handle form submissions with AJAX
- document.querySelectorAll("form").forEach((form) => {
- form.removeEventListener("submit", onFormSubmit);
- form.addEventListener("submit", onFormSubmit);
- });
- });
-
- function onLinkClick(event) {
- var targetUrl = new URL(event.currentTarget.href);
- if (urlIsOnSameSite(targetUrl)) {
- event.preventDefault();
- event.stopPropagation();
- fetch(targetUrl, {redirect: "follow"}).then(handleAjaxResponse);
- }
- }
-
- function onFormSubmit(event) {
- var targetUrl = new URL(event.target.action);
- if (urlIsOnSameSite(targetUrl)) {
- event.preventDefault();
- event.stopPropagation();
- var formData = new FormData(event.target);
- fetch(targetUrl, {redirect: "follow", body: formData, method: event.target.method})
- .then(handleAjaxResponse);
- }
- }
-
- function urlIsOnSameSite(targetUrl) {
- var currentUrl = new URL(window.location.href);
- return targetUrl.origin === currentUrl.origin;
- }
-
- async function handleAjaxResponse(response) {
- // Update URL in browser window, minus request-type field
- var url = new URL(response.url);
- url.searchParams.delete("request-type");
-
- // Get page content from XML response
- var text = await response.text();
- window.history.pushState(text, "", url);
-
- updatePageState(text);
- }
-
- function updatePageState(data) {
- if (!data) {
- fetch(window.location.href, {redirect: "follow"}).then(handleAjaxResponse);
- return;
- }
- var parser = new DOMParser();
- data = parser.parseFromString(data, "text/html");
- var newMainDiv = data.getElementById("main");
- var oldMainDiv = document.getElementById("main");
- document.body.replaceChild(newMainDiv, oldMainDiv);
- document.title = data.title;
-
- // Load inline scripts (DOMParser disables these by default)
- var scripts = document.getElementById("main").getElementsByTagName("script");
- for (const script of scripts) {
- var newScript = document.createElement("script");
- newScript.type = script.type;
- newScript.text = script.text;
- script.parentElement.replaceChild(newScript, script);
- }
-
- // Delete old color picker (will be recreated on DOMContentLoaded)
- document.getElementById("clr-picker").remove();
-
- // Trigger event to signal new page has loaded
- var event = new Event("DOMContentLoaded");
- document.dispatchEvent(event);
-
- // Refresh navbar in case logged-in status changed
- updateNavbar(newMainDiv.dataset.username);
- }
-
- window.addEventListener("popstate", (event) => updatePageState(event.state));
-
- function updateNavbar(username) {
- var loggedIn = username ? true : false;
- document.querySelectorAll(".nav-logged-in").forEach((e) => {e.hidden = !loggedIn;});
- document.querySelectorAll(".nav-logged-out").forEach((e) => {e.hidden = loggedIn;});
- if (loggedIn) {
- document.getElementById("logged-in-status").innerText = `Signed in as ${username}`;
- }
- }
-
- document.addEventListener("DOMContentLoaded", (event) => {
- var mainDiv = document.getElementById("main");
- var rootStyle = document.documentElement.style;
- rootStyle.setProperty("--yellow", mainDiv.dataset.bgcolor);
- rootStyle.setProperty("--black", mainDiv.dataset.fgcolor);
- rootStyle.setProperty("--purple", mainDiv.dataset.accolor);
-
- updateImageColors();
- });
- </script>
+ {%- block head %}{% endblock %}
</head>
<body>
<!-- Embedded image data -->
{{ gif_data|safe }}
- <script>
-
- function customImage(element) {
- // Customize an image by performing a palette swap on the .gif
- // file. The source element must contain a data-img-b64 attribute
- // containing the base64 representation of a .gif file. The byte
- // indexes match .gifs from Aseprite, and may not work for all
- // .gif files.
-
- var style = window.getComputedStyle(document.body);
- var bgcolor = style.getPropertyValue("--yellow");
- var accolor = style.getPropertyValue("--purple");
-
- // Convert base64 string to Uint8Array so we can modify it
- var data = atob(element.dataset.imgB64);
- var bytes = Uint8Array.from(data, c => c.charCodeAt(0));
-
- // Replace background color palette bytes in gif file
- bytes[16] = parseInt(bgcolor.substring(1, 3), 16);
- bytes[17] = parseInt(bgcolor.substring(3, 5), 16);
- bytes[18] = parseInt(bgcolor.substring(5, 7), 16);
-
- // Replace foreground color palette bytes in gif file
- bytes[19] = parseInt(accolor.substring(1, 3), 16);
- bytes[20] = parseInt(accolor.substring(3, 5), 16);
- bytes[21] = parseInt(accolor.substring(5, 7), 16);
-
- // Convert Uint8Array back to base64 so we can use it in a src string
- data = btoa(String.fromCharCode(...bytes));
-
- // Embed base64 in a data string that can be used as an img src.
- return `data:image/gif;base64, ${data}`;
- }
-
- function updateImageColors() {
- // Perform a palette swap on all gifs based on current page colors
- document.querySelectorAll(".img-data").forEach(e => {
- document.querySelectorAll(`.${e.id}`).forEach(t => {
- t.src = customImage(e);
- });
- });
- }
- </script>
-
<div class="page-header">
<div style="text-align: center;">
<img class="title-image littlesongplace02">
</div>
</div>
- {% if "username" in session %}
- <!-- Periodically update activity status indicator -->
- <script>
- updateNavbar("{{ session["username"] }}");
-
- async function checkForNewActivity() {
- const indicator = document.getElementById("activity-indicator")
- const response = await fetch("/new-activity");
- if (!response.ok) {
- console.log(`Failed to get activity: ${response.status}`);
- }
- const json = await response.json();
- indicator.hidden = !json.new_activity;
- }
-
- // Check for new activity every 10s
- setInterval(checkForNewActivity, 10000);
- checkForNewActivity(); // Check immediately
- </script>
- {% endif %}
-
- <!-- Stite Status Messages -->
- {% with messages = get_flashed_messages(with_categories=True) %}
- {% if messages %}
- <div class="flashes">
- <ul>
- {% for category, message in messages %}
- <li class="flash-msg {{ category }}">{{ message }}</li>
- {% endfor %}
- </ul>
+ <!-- Flashed Status Messages -->
+ <div id="flashes-container">
+ {% with messages = get_flashed_messages(with_categories=True) -%}
+ {% if messages -%}
+ <div class="flashes">
+ <ul>
+ {% for category, message in messages %}
+ <li class="flash-msg {{ category }}">{{ message }}</li>
+ {% endfor %}
+ </ul>
+ </div>
+ {%- endif %}
+ {%- endwith %}
</div>
- {% endif %}
- {% endwith %}
<!-- Page-Specific Content -->
- <div class="main" id="main" data-bgcolor="{{ bgcolor }}" data-fgcolor="{{ fgcolor }}" data-accolor="{{ accolor }}" data-username="{{ session['username'] }}">
- {% block body %}
- {% endblock %}
- </div>
+ <div class="main" id="main" data-bgcolor="{{ bgcolor }}" data-fgcolor="{{ fgcolor }}" data-accolor="{{ accolor }}" data-username="{{ session['username'] }}">{% block body %}{% endblock %}</div>
<!-- Padding to prevent player from obscuring content -->
<div id="scroll-padding"></div>
<h2>Recently Uploaded Songs</h2>
<p>Listen to all the newest tunes!</p>
-{{ song_list|safe }}
+{% include "song-list.html" %}
{% endblock %}
{%- endif %}
-{{ song_list|safe }}
+{% include "song-list.html" %}
{% if session["userid"] == userid -%}
<!-- Drag-and-drop playlist editor -->
{% endif %}
<!-- Song List -->
-{{ song_list|safe }}
+{% include "song-list.html" %}
{% endblock %}
<div class="song" data-song="{{ song.json() }}">
<div class="song-main">
<!-- Profile Picture -->
- {% if song.user_has_pfp %}
+ {% if song.user_has_pfp -%}
<img class="small-pfp" src="/pfp/{{ song.userid }}" onerror="this.style.display = 'none'" />
- {% endif %}
+ {%- endif %}
<div class="song-info">
-
</div>
- <!-- Song Artist -->
+ <!-- Song Artist(s) -->
<div class="song-artist">
<a href="/users/{{ song.username }}" class="profile-link">{{ song.username }}</a>
- <!-- Song Collaborators -->
+ <!-- Song Collaborators -->
{% for collab in song.collaborators %}
- {% if collab.startswith("@") %}
- <a href="/users/{{ collab[1:] }}" class="profile-link">{{ collab[1:] }}</a>
- {% else %}
- <span class="collab-name">{{ collab }}</span>
- {% endif %}
+ {% if collab.startswith("@") -%}
+ <a href="/users/{{ collab[1:] }}" class="profile-link">{{ collab[1:] }}</a>
+ {%- else -%}
+ <span class="collab-name">{{ collab }}</span>
+ {%- endif %}
{% endfor %}
</div>
</div>
<div class="song-buttons">
+ {% if session["userid"] == song.userid and is_profile_song_list -%}
<!-- Owner-Specific Buttons (Edit/Delete) -->
- {% if session["userid"] == song.userid and is_profile_song_list %}
<a href="/edit-song?songid={{ song.songid }}" class="song-list-button">
<img class="lsp_btn_edit02" alt="Edit">
</a>
<a href="/delete-song/{{ song.songid }}" onclick="return confirm("Are you sure you want to delete this song?")" class="song-list-button">
<img class="lsp_btn_delete02" alt="Delete">
</a>
- {% endif %}
+ {%- endif %}
<!-- Details Button -->
- <button onclick="return showDetails(event)" class="song-list-button">
+ <button onclick="return showDetails(event)" class="song-list-button details-toggle">
<img class="lsp_btn_show02" alt="Show Details">
</button>
</div>
<div class="song-details" {% if request.endpoint != 'song' %}hidden{% endif %}>
- {% if current_user_playlists %}
+ {% if current_user_playlists -%}
<!-- Add to Playlist Buttons -->
<div class="song-playlist-controls">
<form action="/append-to-playlist" method="post">
</select>
</form>
</div>
- {% endif %}
+ {%- endif %}
+ {% if song.description -%}
<!-- Song Description -->
- {% if song.description %}
<div class="song-description">{{ (song.description.replace("\n", "<br>"))|safe }}</div>
- {% endif %}
+ {%- endif %}
+ {% if song.tags -%}
<!-- Song Tags -->
<div class="song-tags">
Tags:
<a href="/songs?user={{ song.username }}&tag={{ tag }}">{{ tag }}</a>
{% endfor %}
</div>
+ {%- endif %}
<div class="song-date">
Uploaded {{ song.created }}
function showDetails(event) {
var songElement = event.target.closest(".song");
var songDetails = songElement.querySelector(".song-details");
+ var detailsToggle = songElement.querySelector(".details-toggle img");
if (songDetails.hidden) {
// Show details
songDetails.hidden = false;
- event.target.alt = "Hide Details";
- event.target.className = "lsp_btn_hide02";
- event.target.src = customImage(document.getElementById("lsp_btn_hide02"));
+ detailsToggle.alt = "Hide Details";
+ detailsToggle.className = "lsp_btn_hide02";
+ detailsToggle.src = customImage(document.getElementById("lsp_btn_hide02"));
}
else {
// Hide details
songDetails.hidden = true;
- event.target.alt = "Show Details";
- event.target.className = "lsp_btn_show02";
- event.target.src = customImage(document.getElementById("lsp_btn_show02"));
+ detailsToggle.alt = "Show Details";
+ detailsToggle.className = "lsp_btn_show02";
+ detailsToggle.src = customImage(document.getElementById("lsp_btn_show02"));
}
return false;
}
{% block body %}
-{{ song_list|safe }}
+{% include "song-list.html" %}
{% endblock %}
</div>
{% endif %}
-{{ song_list|safe }}
+{% include "song-list.html" %}
{% endblock %}