BGCOLOR = "#e8e6b5"
-FGCOLOR = "#695c73"
-ACCOLOR = "#9373a9"
+FGCOLOR = "#514659"
+ACCOLOR = "#b36fab"
DEFAULT_COLORS = dict(bgcolor=BGCOLOR, fgcolor=FGCOLOR, accolor=ACCOLOR)
class Song:
songid: int
userid: int
+ user: users.User
threadid: int
username: str
title: str
event_title: Optional[str]
def json(self):
- return json.dumps(vars(self))
+ vs = vars(self)
+ return json.dumps({k: vs[k] for k in vs if not isinstance(vs[k], users.User)})
def get_comments(self):
return comments.for_thread(self.threadid)
songs.append(Song(
songid=sd["songid"],
userid=sd["userid"],
+ user=users.User.from_row(sd),
threadid=sd["threadid"],
username=sd["username"],
title=sd["title"],
// Check for new activity every 10s
setInterval(checkForNewActivity, 10000);
-function customImage(element) {
+function customImage(source, target) {
// 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 style = window.getComputedStyle(target);
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 data = atob(source.dataset.imgB64);
var bytes = Uint8Array.from(data, c => c.charCodeAt(0));
// Replace background color palette bytes in gif file
// 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);
+ t.src = customImage(e, t);
});
});
}
var miniButton = document.getElementById("mini-play-pause-button");
audio.addEventListener("play", (event) => {
button.className = "lsp_btn_pause02";
- button.src = customImage(document.getElementById("lsp_btn_pause02"));
+ button.src = customImage(document.getElementById("lsp_btn_pause02"), button);
miniButton.className = "lsp_btn_pause02";
- miniButton.src = customImage(document.getElementById("lsp_btn_pause02"));
+ miniButton.src = customImage(document.getElementById("lsp_btn_pause02"), button);
})
// Show play button when audio is paused
audio.addEventListener("pause", (event) => {
button.className = "lsp_btn_play02";
- button.src = customImage(document.getElementById("lsp_btn_play02"));
+ button.src = customImage(document.getElementById("lsp_btn_play02"), button);
miniButton.className = "lsp_btn_play02";
- miniButton.src = customImage(document.getElementById("lsp_btn_play02"));
+ miniButton.src = customImage(document.getElementById("lsp_btn_play02"), button);
})
// Audio position scrubbing
+/* Fonts */
+@font-face {
+ font-family: "Mat Saleh";
+ src: url("/static/MatSaleh.otf");
+}
+
+@font-face {
+ font-family: "Unageo";
+ src: url("/static/Unageo-Medium.ttf");
+}
+
+@font-face {
+ font-family: "Unageo";
+ src: url("/static/Unageo-Medium-Italic.ttf");
+ font-style: italic;
+}
+
+@font-face {
+ font-family: "Unageo";
+ src: url("/static/Unageo-ExtraBold.ttf");
+ font-weight: bold;
+}
+
+@font-face {
+ font-family: "Unageo";
+ src: url("/static/Unageo-ExtraBold-Italic.ttf");
+ font-weight: bold;
+ font-style: italic;
+}
+
/* General */
:root {
--yellow: #e8e6b5;
- --purple: #9373a9;
+ --purple: #b36fab;
--blue: #44b7b7;
- --black: #695c73;
+ --black: #514659;
+ --radius: 5px;
}
body {
background: var(--yellow);
color: var(--black);
- font-family: sans-serif;
+ font-family: "Unageo", sans-serif;
border-color: var(--purple);
max-width: 700px;
margin: auto;
h1, h2, h3, h4 {
color: var(--purple);
+ font-family: "Mat Saleh", sans-serif;
+}
+
+h1 {
+ font-size: 40px;
+ text-align: center;
}
h2 {
- font-size: 20px;
+ font-size: 26px;
+ /*border: 2px solid var(--purple);*/
+ box-shadow: 3px 3px 0px 0px var(--black);
+ background: var(--purple);
+ color: var(--yellow);
+ border-radius: var(--radius);
+ text-align: center;
+ padding: 5px;
}
h3 {
- font-size: 16px;
+ font-size: 22px;
}
textarea {
- font-family: sans-serif;
+ font-family: "Unageo", sans-serif;
font-size: 16px;
color: var(--black);
border: 3px solid var(--purple);
- border-radius: 10px;
+ border-radius: var(--radius);
padding: 10px;
background: var(--yellow);
height: 200px;
input[type=text], input[type=password], input[type=url], input[type=datetime-local] {
background: var(--yellow);
margin: 10px;
- font-family: sans-serif;
+ font-family: "Unageo", sans-serif;
font-size: 16px;
color: var(--black);
border: 3px solid var(--purple);
- border-radius: 10px;
+ border-radius: var(--radius);
padding: 5px;
width: 300px;
max-width: calc(100% - 40px);
.button, input[type=submit] {
cursor: pointer;
- font-family: sans-serif;
+ font-family: "Unageo", sans-serif;
font-size: 16px;
font-weight: bold;
text-decoration: none;
color: var(--yellow);
background: var(--purple);
border: 0px;
- border-radius: 5px;
+ border-radius: var(--radius);
padding: 8px;
}
border: none;
background-color: var(--purple);
color: var(--yellow);
- border-radius: 5px;
+ border-radius: var(--radius);
font-size: 16px;
padding: 8px;
}
}
div.page-header {
- box-shadow: 0px 0px 20px 0px;
- border-radius: 10px;
+ box-shadow: 5px 5px 0px 0px var(--black);
+ border: 1px solid var(--black);
+ border-radius: var(--radius);
margin: 10px;
+ padding: 5px;
}
.flashes {
border: 3px solid var(--blue);
- border-radius: 10px;
+ border-radius: var(--radius);
margin: 20px;
}
box-sizing: border-box;
margin: 10px;
border-width: 3px;
- border-radius: 10px;
+ border-radius: var(--radius);
border-style: solid;
padding: 10px;
background-color: var(--yellow);
display: flex;
flex-wrap: wrap;
gap: 10px;
+ justify-content: center;
}
.user-list-entry-container {
- border-radius: 10px;
- box-shadow: 0px 0px 5px 0px;
+ border-radius: var(--radius);
+ background: var(--black);
+ box-shadow: 2px 2px 0px 0px var(--black);
+ border: 1px solid var(--black);
+ overflow: hidden;
}
.user-list-entry {
box-sizing: border-box;
height: 100%;
background: var(--yellow);
- border-radius: 10px;
+ /*border-radius: var(--radius);*/
padding: 5px;
display: flex;
max-width: 40%;
background: var(--purple);
padding: 5px;
- border-radius: 10px;
+ border-radius: var(--radius);
}
.big-pfp {
margin: 0px;
padding: 0px;
display: block;
- border-radius: 5px;
+ border-radius: calc(var(--radius) / 2);
}
.small-pfp {
max-width: 32px;
max-height: 32px;
- border-radius: 5px;
+ border-radius: calc(var(--radius) / 2);
border: solid 2px var(--purple);
background-color: var(--purple);
vertical-align: middle;
}
.playlist-list-entry {
- box-shadow: 0px 0px 5px 0px;
- border-radius: 10px;
+ box-shadow: 3px 3px 0px 0px;
+ border-radius: var(--radius);
+ border: 1px solid var(--black);
padding: 10px;
margin: 10px 0px;
}
.draggable-song {
box-shadow: 0px 0px 5px 0px;
- border-radius: 10px;
+ border-radius: var(--radius);
padding: 5px 10px;
margin: 10px 0px;
display: flex;
height: 22px !important;
left: 7px;
right: auto;
- border-radius: 5px;
+ border-radius: calc(var(--radius) / 2);
}
.clr-field input {
display: flex;
flex-direction: column;
gap: 10px;
+ font-size: 14px;
}
-.song-list-songs .song {
- box-shadow: 0px 0px 5px 0px;
- border-radius: 10px;
+div.song {
+ background: var(--yellow);
+ box-shadow: 2px 2px 0px 0px;
+ border-radius: var(--radius);
+ border: 1px solid var(--black);
+ overflow: hidden;
}
div.song-main {
}
div.song-info {
+ font-family: "Unageo", sans-serif;
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin-top: 10px;
padding: 10px;
box-shadow: 0px 0px 5px 0px;
- border-radius: 10px;
+ border-radius: var(--radius);
}
div.reply-comment {
margin-bottom: 10px;
padding: 10px;
box-shadow: 0px 0px 5px 0px;
- border-radius: 10px;
+ border-radius: var(--radius);
}
div.comment-button-container {
padding: 10px;
padding-bottom: 0px;
box-shadow: 0px 0px 20px 0px;
- border-radius: 10px;
+ border-radius: var(--radius);
background: var(--yellow);
}
margin: 10px;
padding-left: 10px;
box-shadow: 0px 0px 20px 0px;
- border-radius: 10px;
+ border-radius: var(--radius);
background: var(--yellow);
display: flex;
#position-slider::-webkit-slider-thumb {
height: 20px;
width: 20px;
- border-radius: 10px;
+ border-radius: var(--radius);
}
#position-slider::-moz-range-thumb {
height: 20px;
width: 20px;
- border-radius: 10px;
+ border-radius: var(--radius);
}
.player-button img {
margin: 10px;
padding: 10px;
box-shadow: 0px 0px 5px 0px;
- border-radius: 10px;
+ border-radius: var(--radius);
}
/* Platform-specific global overrides */
display: none;
}
}
+
+/* Jam Events */
+.jam-event-list-title {
+ margin-left: 20px;
+}
{% block body %}
-<h1>About littlesong.place</h1>
+<h1>about littlesong.place</h1>
<p>
Hello there, thanks for stopping by!
</p>
+<h3>the little place</h3>
<p>
This site is meant to be a little place for your songs. Of course, there are
already lots of <em>big</em> places for songs out there. But I wanted
somewhere to share music that was free from algorithms, ads, rankings, and the
general noise that comes with most of those big places. A place that
encourages and celebrates creativity, and a place for community with other
-humans. If any of that resonates with you, then welcome! I hope you enjoy
-your stay.
+humans.
+</p>
+<p>
+If any of that resonates with you, then welcome! I hope you enjoy your stay.
</p>
+<h3>the dream</h3>
<p>
I also love <a href="https://indreams.me">Dreams</a>, and the music community
that formed there. But now that live service support for Dreams has ended, and
given the uncertain future of Media Molecule and the games industry more
broadly, I worry that it's only a matter of time before the servers shut down
-for good. Part of my goal with this site is to give others a place to archive
-their music from Dreams. But beyond that, I hope it can also eventually become
-a similar platform for creativity and community of its own - albeit on a much
-smaller scale.
+for good.
+</p>
+<p>
+Part of my goal with this site is to give others a place to archive
+their music from Dreams. But beyond that, my hope is that it can also
+eventually become a platform for creativity and community of its own - albeit
+on a much smaller scale.
</p>
+<h3>the code</h3>
<p>
The site is open source! You can
<a href="https://github.com/cfulljames/littlesongplace">view it on GitHub</a>.
</p>
-<h2>Contact Me!</h2>
+<h2>contact me!</h2>
<p>
I'm always looking for new ways to improve the site! If you have ideas (or if
{% block body %}
{% if comments %}
-<h1>Activity</h1>
+<h1>activity</h1>
{% for comment in comments %}
<div class="comment-notification">
<div class="page-header">
<div style="text-align: center;">
- <img class="title-image littlesongplace02">
+ <!--<img class="title-image littlesongplace02">-->
+ <h1>little song place</h1>
</div>
<!-- Navbar -->
<div class="navbar">
<a href="/">Home</a>
+ <a href="/jams">Jams</a>
<a href="/about">About</a>
<a href="/site-news">News</a>
<a href="/songs">Random</a>
{% block body %}
-<h1>Write a Comment</h1>
+<h1>write a comment</h1>
<p>
{% if song -%}
{% block body %}
-<h2>Hello!</h2>
-<p>
-🎶 Welcome to a little song place. Make music, and share it with friends! 🎵
-</p>
+<h2>hello!</h2>
+<div style="display: flex; flex-direction: row; justify-content: center; gap: 10px; align-items: center;">
+<div>🎶</div>
+<div style="text-align: center;">Welcome to littlesong.place.<br/>Make music, and share it with friends!</div>
+<div>🎵</div>
+</div>
{% if ongoing_events or upcoming_events %}
-<h2>Jams</h2>
+<h2>happenings</h2>
{% from "jam-event-list.html" import jam_event_list %}
-{{ jam_event_list("Ongoing Events", ongoing_events, "Ends", "end") }}
-{{ jam_event_list("Upcoming Events", upcoming_events, "Starts", "start") }}
+{{ jam_event_list("ongoing events", ongoing_events, "ends", "end") }}
+{{ jam_event_list("upcoming events", upcoming_events, "starts", "start") }}
{% endif %}
-<h2>Humans</h2>
-<p>
-Check out the music and profiles of the fine folks below!
-{% if not session["userid"] %}If you create an account, you'll show up here too.{% endif %}
-</p>
-
+<h2>humans</h2>
<div class="user-list">
{% for user in users %}
<div class="user-list-entry-container">
{% endfor %}
</div>
-<h2>Hot New Tunes</h2>
-<p>Listen to the 50 latest tracks!</p>
+<h2>hot new tunes</h2>
{% include "song-list.html" %}
{% endblock %}
{% macro jam_event_list(list_title, events, date_label, which_date) %}
{% if events %}
-<h3>{{ list_title }}</h3>
+<h3 class="jam-event-list-title">{{ list_title }}</h3>
<ul class="jam-event-list">
{% for event in events %}
<li class="jam-event-list-entry">
<a href="/jams/{{ event.jamid }}/events/{{ event.eventid }}">[Upcoming Event]</a>
</span>
{%- else -%}
- <span class="jam-event-list-title">
+ <span>
<a href="/jams/{{ event.jamid }}/events/{{ event.eventid }}">{{ event.title }}</a>
</span>
{%- endif %}
<strong> | </strong>
- <span class="jam-event-list-jam-title">
+ <span>
<a href="/jams/{{ event.jamid }}">{{ event.jam_title }}</a>
</span>
<strong> | </strong>
{% block body %}
-<h1>{% if event.hidden -%}[Upcoming Event]{%- else -%}{{ event.title }}{%- endif %} - {{ jam.title }}</h1>
+<h1>{% if event.hidden -%}[Upcoming Event]{%- else -%}{{ event.title }}{%- endif %}</h1>
<div id="jam-event-view">
<div class="info">
- <strong>Host:</strong>
+ <strong>jam:</strong>
+ <a href="/jams/{{ jam.jamid }}">{{ jam.title }}</a>
+ <br/>
+ <strong>host:</strong>
<a href="/users/{{ jam.ownername }}" class="profile-link">{{ jam.ownername }}</a>
<br/>
{% if event.startdate %}
- <strong>Start Date:</strong>
+ <strong>start date:</strong>
<span class="date" data-date="{{ event.startdate.isoformat() }}"><!-- Populated by nav.js --></span>
<br/>
{% endif %}
{% if event.enddate %}
- <strong>End Date:</strong>
+ <strong>end date:</strong>
<span class="date" data-date="{{ event.enddate.isoformat() }}"><!-- Populated by nav.js --></span>
{% endif %}
</div>
</div>
<h2>About the Jam</h2>
- <div>
- <strong><a href="/jams/{{ jam.jamid }}">{{ jam.title }}</a></strong>
- </div>
- <br/>
<div>
{{ (jam.description.replace("\n", "<br>"))|safe }}
</div>
<div id="jam-view">
<div>
- <strong>Host:</strong>
+ <strong>host:</strong>
<a href="/users/{{ jam.ownername }}" class="profile-link">{{ jam.ownername }}</a>
</div>
<br/>
</div>
{%- endif %}
- <h2>Description</h2>
+ <h2>description</h2>
<div>
{{ (jam.description.replace("\n", "<br>"))|safe }}
</div>
- <h2>Events</h2>
+ <h2>events</h2>
{%- if jam.ownerid == session["userid"] -%}
<a class="song-list-button" title="Create Event" href="/jams/{{ jam.jamid }}/events/create"><img class="lsp_btn_add02" /></a>
{%- endif -%}
{% from "jam-event-list.html" import jam_event_list %}
- {{ jam_event_list("Ongoing Events", ongoing, "Ends", "end") }}
- {{ jam_event_list("Upcoming Events", upcoming, "Starts", "start") }}
- {{ jam_event_list("Past Events", past, "Ended", "end") }}
+ {{ jam_event_list("ongoing events", ongoing, "ends", "end") }}
+ {{ jam_event_list("upcoming events", upcoming, "starts", "start") }}
+ {{ jam_event_list("past events", past, "ended", "end") }}
</div> <!-- jam-view -->
{% if session["userid"] == jam.ownerid -%}
{% block body %}
-<h1>Jams</h1>
+<h1>jams</h1>
-<h2>Events</h2>
+<h2>events</h2>
{% from "jam-event-list.html" import jam_event_list %}
-{{ jam_event_list("Ongoing Events", ongoing, "Ends", "end") }}
-{{ jam_event_list("Upcoming Events", upcoming, "Starts", "start") }}
-{{ jam_event_list("Recent Events", past, "Ended", "end") }}
+{{ jam_event_list("ongoing events", ongoing, "ends", "end") }}
+{{ jam_event_list("upcoming events", upcoming, "starts", "start") }}
+{{ jam_event_list("recent events", past, "ended", "end") }}
-<h2>All Jams</h2>
+<h2>all jams</h2>
<ul class="jam-list">
{% for jam in jams -%}
<li><a href="/jams/{{ jam.jamid }}">{{ jam.title }}</a></li>
{% block body %}
-<h1>Sign In</h1>
+<h1>sign in</h1>
{% if note %}
<div class="note">{{ note }}</div>
{% block body %}
-<h1>Site News</h1>
+<h1>site news</h1>
-<h2>2025-04-20 - little song jam</h2>
+<h2>2025-04-26 - little song jam</h2>
<p>
-Get ready! The first little song jam starts next week! I've been hard at work
-building the new jam system for the site, and it's finally done.
+Get ready! The first little song jam is here! I've been hard at work building
+the new jam system for the site, and it's finally done.
</p>
<p>
The current plan is to host a week-long jam once a month. The theme will be
<ul>
<li>New <a href="/jams/1">jam page</a>!</li>
+ <li>New fonts</li>
+ <li>Lots of visual tweaks</li>
</ul>
<h2>2025-04-06 - Open Source</h2>
{% if session["userid"] == userid or playlists -%}
<div id="profile-playlists">
- <h2>Playlists</h2>
+ <h2>playlists</h2>
<!-- Add Playlist button/form -->
{% if session["userid"] == userid -%}
{% if session["userid"] == userid or songs %}
<div id="profile-songs">
- <h2>Songs</h2>
+ <h2>songs</h2>
<!-- Add Song button -->
{% if session["userid"] == userid %}
{% endif %}
-<h2>Comments</h2>
+<h2>comments</h2>
{{ comment_thread(threadid, session['userid'], userid, comments) }}
{% endblock %}
{% block title %}Create Account{% endblock %}
{% block body %}
-<h1>Create a new account</h1>
+<h1>create a new account</h1>
<p>Welcome to the site!</p>
<form method="post">
<div class="signup-form">
songDetails.hidden = false;
detailsToggle.alt = "Hide Details";
detailsToggle.className = "lsp_btn_hide02";
- detailsToggle.src = customImage(document.getElementById("lsp_btn_hide02"));
+ detailsToggle.src = customImage(document.getElementById("lsp_btn_hide02"), detailsToggle);
}
else {
// Hide details
songDetails.hidden = true;
detailsToggle.alt = "Show Details";
detailsToggle.className = "lsp_btn_show02";
- detailsToggle.src = customImage(document.getElementById("lsp_btn_show02"));
+ detailsToggle.src = customImage(document.getElementById("lsp_btn_show02"), detailsToggle);
}
return false;
}
{% block body %}
-<h1>Songs</h1>
+<h1>songs</h1>
{% if user %}
<div class="filter">
+from dataclasses import dataclass
+
from . import colors, datadir, db
+@dataclass
+class User:
+ userid: int
+ username: str
+ fgcolor: str
+ bgcolor: str
+ accolor: str
+
+ @property
+ def colors(self):
+ return {
+ "fgcolor": self.fgcolor,
+ "bgcolor": self.bgcolor,
+ "accolor": self.accolor,
+ }
+
+ @classmethod
+ def from_row(cls, row):
+ user_colors = get_user_colors(row)
+ return User(
+ userid=row["userid"],
+ username=row["username"],
+ **user_colors)
+
+def by_id(userid):
+ user_data = db.query("select * from users where userid = ?", [userid], one=True)
+ return User.from_row(user_data)
+
def user_has_pfp(userid):
return (datadir.get_user_images_path(userid)/"pfp.jpg").exists()
def test_signup_get(client):
response = client.get("/signup")
- assert b"Create a new account" in response.data
+ assert b"create a new account" in response.data
def test_signup_success(client):
response = post_signup_form(client, "user", "password")
_assert_appear_in_order(
response.data,
[
- b"Ongoing Events",
+ b"ongoing events",
b"OngoingJam",
- b"Upcoming Events",
+ b"upcoming events",
b"[Upcoming Event]", # Upcoming jam name hidden
b"[Upcoming Event]", # Upcoming jam name hidden
- b"Recent Events",
+ b"recent events",
b"PastJam",
- b"All Jams",
+ b"all jams",
b"New Jam",
b"New Jam",
])
_assert_appear_in_order(
response.data,
[
- b"Ongoing Events",
+ b"ongoing events",
b"OngoingJam",
- b"Upcoming Events",
+ b"upcoming events",
b"[Upcoming Event]", # Upcoming jam name hidden
- b"Past Events",
+ b"past events",
b"PastJam",
])