]> littlesong.place Git - littlesongplace.git/commitdiff
Add playlist editor
authorChris Fulljames <christianfulljames@gmail.com>
Sat, 15 Feb 2025 19:07:22 +0000 (14:07 -0500)
committerChris Fulljames <christianfulljames@gmail.com>
Sat, 15 Feb 2025 19:07:22 +0000 (14:07 -0500)
main.py
schema_update.sql
static/styles.css
templates/playlist.html
templates/profile.html
templates/song-list.html

diff --git a/main.py b/main.py
index dad5977a4f1a0c16522ff2e7f9fa4c632f6d30bb..aee0141471709b7f846a9b437fc9981b2b786d0a 100644 (file)
--- a/main.py
+++ b/main.py
@@ -179,7 +179,7 @@ def users_profile(profile_username):
             user_accolor=profile_data["accolor"],
             playlists=plist_data,
             songs=songs,
-            song_list=render_template("song-list.html", songs=songs))
+            song_list=render_template("song-list.html", songs=songs, is_profile_song_list=True))
 
 @app.post("/edit-profile")
 def edit_profile():
@@ -727,13 +727,13 @@ def create_playlist():
     flash_and_log(f"Created playlist {name}", "success")
     return redirect(request.referrer)
 
-@app.post("/delete-playlist/<int:playlistid>")
+@app.get("/delete-playlist/<int:playlistid>")
 def delete_playlist(playlistid):
     if not "userid" in session:
         abort(401)
 
     # Make sure playlist exists
-    plist_data = query_db("select * from playlists where playlistid = ?", args=[playlistid])
+    plist_data = query_db("select * from playlists where playlistid = ?", args=[playlistid], one=True)
     if not plist_data:
         abort(404)
 
@@ -746,7 +746,7 @@ def delete_playlist(playlistid):
     get_db().commit()
 
     flash_and_log(f"Deleted playlist {plist_data['name']}", "success")
-    return redirect(request.referrer)
+    return redirect(f"/users/{session['username']}")
 
 @app.post("/append-to-playlist")
 def append_to_playlist():
@@ -770,7 +770,7 @@ def append_to_playlist():
     songid = request.form["songid"]
 
     # Make sure song exists
-    song_data = query_db("select * from songs where songid = ?", args=[songid])
+    song_data = query_db("select * from songs where songid = ?", args=[songid], one=True)
     if not song_data:
         abort(404)
 
@@ -786,7 +786,9 @@ def append_to_playlist():
     query_db("update playlists set updated = ? where playlistid = ?", args=[timestamp, playlistid])
     get_db().commit()
 
-    return {"status": "ok"}
+    flash_and_log(f"Added '{song_data['title']}' to {plist_data['name']}", "success")
+
+    return redirect(request.referrer)
 
 @app.post("/edit-playlist/<int:playlistid>")
 def edit_playlist_post(playlistid):
@@ -794,7 +796,7 @@ def edit_playlist_post(playlistid):
         abort(401)
 
     # Make sure playlist exists
-    plist_data = query_db("select * from playlists where playlistid = ?", args=[playlistid])
+    plist_data = query_db("select * from playlists where playlistid = ?", args=[playlistid], one=True)
     if not plist_data:
         abort(404)
 
@@ -802,32 +804,30 @@ def edit_playlist_post(playlistid):
     if session["userid"] != plist_data["userid"]:
         abort(401)
 
-    private = request.form["type"] == "private"
-
     # Make sure all songs are valid
-    songids = []
-    for field, value in request.form.items():
-        if field.startswith("position-"):
-            try:
-                position = int(field[len("position-"):])
-                songid = int(value)
-            except ValueError:
-                abort(400)
-
-            song_data = query_db("select * from songs where songid = ?", args=[songid])
-            if not song_data:
-                abort(400)
-
-            # Song is valid, add to list
-            songids.append((position, songid))
+    try:
+        songids = [int(s) for s in request.form["songids"].split(",")]
+    except ValueError:
+        # Invalid songid(s)
+        abort(400)
+
+    for songid in songids:
+        song_data = query_db("select * from songs where songid = ?", args=[songid])
+        if not song_data:
+            abort(400)
 
     # All songs valid - delete old songs
     query_db("delete from playlist_songs where playlistid = ?", args=[playlistid])
 
     # Re-add songs with new positions
-    for position, songid in songids:
+    for position, songid in enumerate(songids):
         query_db("insert into playlist_songs (playlistid, position, songid) values (?, ?, ?)", args=[playlistid, position, songid])
 
+    # Update private, name
+    private = int(request.form["type"] == "private")
+    name = request.form["name"]
+    query_db("update playlists set private = ?, name = ? where playlistid = ?", [private, name, playlistid])
+
     get_db().commit()
 
     flash_and_log("Playlist updated", "success")
@@ -855,7 +855,10 @@ def playlists(playlistid):
     return render_template(
             "playlist.html",
             name=plist_data["name"],
+            playlistid=plist_data["playlistid"],
+            userid=plist_data["userid"],
             username=plist_data["username"],
+            songs=songs,
             song_list=render_template("song-list.html", songs=songs))
 
 def flash_and_log(msg, category=None):
@@ -922,6 +925,7 @@ def get_db():
     db = getattr(g, '_database', None)
     if db is None:
         db = g._database = sqlite3.connect(DATA_DIR / "database.db")
+        db.cursor().execute("PRAGMA foreign_keys = ON")
 
         # Get current version
         user_version = query_db("pragma user_version", one=True)[0]
index 44b610c10c9c0036b6ec9c4feeae0f78418cc676..3d065f29263e39712ea3c97669f5a62fb9a24e6c 100644 (file)
@@ -6,7 +6,7 @@ CREATE TABLE playlists (
     name TEXT NOT NULL,
     private INTEGER NOT NULL,
 
-    FOREIGN KEY(userid) REFERENCES users(userid)
+    FOREIGN KEY(userid) REFERENCES users(userid) ON DELETE CASCADE
 );
 CREATE INDEX playlists_by_userid ON playlists(userid);
 
@@ -16,8 +16,8 @@ CREATE TABLE playlist_songs (
     songid INTEGER NOT NULL,
 
     PRIMARY KEY(playlistid, position),
-    FOREIGN KEY(playlistid) REFERENCES playlists(playlistid),
-    FOREIGN KEY(songid) REFERENCES songs(songid)
+    FOREIGN KEY(playlistid) REFERENCES playlists(playlistid) ON DELETE CASCADE,
+    FOREIGN KEY(songid) REFERENCES songs(songid) ON DELETE CASCADE
 );
 CREATE INDEX playlist_songs_by_playlist ON playlist_songs(playlistid);
 
index 130bc63a5baf9a3fb61b1b7b9b284b3e80d036dd..54aeb80917a427a3f704df2ce97557c75dabc08b 100644 (file)
@@ -65,7 +65,15 @@ input[type=text], input[type=password], input[type=url] {
     border: 0px;
     border-radius: 5px;
     padding: 8px;
-    margin: 10px;
+}
+
+select {
+    border: none;
+    background-color: var(--purple);
+    color: var(--yellow);
+    border-radius: 5px;
+    font-size: 16px;
+    padding: 8px;
 }
 
 div.main {
@@ -197,6 +205,10 @@ input[type=file] {
     border: solid 2px var(--purple);
 }
 
+.profile-bio {
+    margin: 10px 0px;
+}
+
 .profile-action {
     margin-bottom: 20px;
 }
@@ -209,6 +221,24 @@ input[type=file] {
     margin: 10px;
 }
 
+.playlist-list-entry {
+    box-shadow: 0px 0px 5px 0px;
+    border-radius: 10px;
+    padding: 10px;
+    margin: 10px 0px;
+}
+
+.draggable-song {
+    box-shadow: 0px 0px 5px 0px;
+    border-radius: 10px;
+    padding: 5px 10px;
+    margin: 10px 0px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    gap: 10px;
+}
+
 /* Coloris Color Picker */
 
 .clr-field button {
index e298333d05afb3bea284baa98a082b40b53e5ca4..f2f622a9002291d5a67ecfae304488ce0db144ee 100644 (file)
 
 {% block title %}{{ name }}{% endblock %}
 
-{% block body %}
+{% block body -%}
 
 <h1>{{ name }}</h1>
 
 <p>Playlist by <a href="/users/{{ username }}" class="profile-link">{{ username }}</a></p>
 
+{% if session["userid"] == userid -%}
+<p class="playlist-actions">
+<button class="button" onclick="showPlaylistEditor()">Edit</button>
+<button class="button" onclick="deletePlaylist()">Delete</button>
+</p>
+
+<script>
+function showPlaylistEditor() {
+    document.querySelector(".song-list").hidden = true;
+    document.querySelector(".playlist-actions").hidden = true;
+    document.querySelector(".playlist-editor").hidden = false;
+}
+
+function hidePlaylistEditor() {
+    document.querySelector(".song-list").hidden = false;
+    document.querySelector(".playlist-actions").hidden = false;
+    document.querySelector(".playlist-editor").hidden = true;
+}
+
+function deletePlaylist() {
+    if (confirm("Are you sure you want to delete this playlist?")) {
+        window.location.href = "/delete-playlist/{{ playlistid }}";
+    }
+}
+</script>
+
+{%- endif %}
+
 {{ song_list|safe }}
 
-{% endblock %}
+{% if session["userid"] == userid -%}
+<!-- Drag-and-drop playlist editor -->
+<div class="playlist-editor" hidden>
+    <h2>Edit Playlist</h2>
+    <form action="/edit-playlist/{{ playlistid }}" method="post" onsubmit="updateSongIds(event)">
+        <label for="name">Playlist Name</label><br>
+        <input name="name" type="text" maxlength="100" value="{{ name }}"/><br>
+
+        <label for="type">Playlist Type:</label>
+        <input name="type" type="radio" value="private" checked />
+        <label for="private">Private</label>
+        <input name="type" type="radio" value="public" />
+        <label for="public">Public</label><br>
+
+        <input id="playlist-songids-input" type="hidden" name="songids" value="-1" /> <!-- Populated by script on submit -->
+
+        <p>Drag and drop songs to reorder them, or use the trash icon to remove them from the playlist.</p>
+
+        <div class="edit-list">
+            {%- for song in songs %}
+            <div class="draggable-song" draggable="true" ondragstart="onSongDragStart(event)" ondragover="onSongDragOver(event)" ondrop="onSongDrop(event)">
+                <span class="songid" hidden>{{ song.songid }}</span>
+                <span class="song-title">{{ song.title }}</span> -
+                <span class="song-artist">{{ song.username }}</span>
+                <span style="width: 100%"></span>
+                <button onclick="removeSong(event)" class="song-list-button" title="Remove" style="margin-right: 0px">
+                    <img class="lsp_btn_delete02" alt="Delete">
+                </button>
+            </div>
+            {%- endfor %}
+
+            <!-- dummy song to move to end -->
+            <div class="draggable-song" ondragover="onSongDragOver(event)" ondrop="onSongDrop(event)">
+                &nbsp;
+            </div>
+        </div>
+
+        <a href="javascript:hidePlaylistEditor()">Cancel</a>
+        <input type="submit" value="Save" style="margin: 10px;"/>
+    </form>
+
+    <script>
+    function updateSongIds(event) {
+        var form = event.currentTarget;
+        var editList = form.querySelector(".edit-list");
+        var songids = [];
+        for (const entry of editList.children) {
+            var songidSpan = entry.querySelector(".songid");
+            if (songidSpan) {
+                console.log(songidSpan.textContent);
+                songids.push(songidSpan.textContent);
+            }
+        }
+        songids = songids.join(",");
+        console.log(songids);
+
+        var songidsInput = form.querySelector("#playlist-songids-input");
+        songidsInput.value = songids;
+    }
+    function onSongDragStart(event) {
+        var list = event.currentTarget.closest(".edit-list");
+        var index = [...list.children].indexOf(event.currentTarget);
+        event.dataTransfer.setData("text", index.toString());
+        event.dataTransfer.effectAllowed = "move";
+    }
+    function onSongDragOver(event) {
+        event.preventDefault();
+        event.dataTransfer.dropEffect = "move";
+        var list = event.currentTarget.closest(".edit-list");
+        for (const child of list.children) {
+            child.style.borderTop = "";
+            child.style.borderBottom = "";
+        }
+        if (event.currentTarget.previousElementSibling) {
+            event.currentTarget.previousElementSibling.style.borderBottom = "3px solid var(--purple)";
+        }
+        event.currentTarget.style.borderTop = "3px solid var(--purple)";
+    }
+    function onSongDrop(event) {
+        event.preventDefault();
+        const data = event.dataTransfer.getData("text");
+        var sourceIndex = parseInt(data);
+        var list = event.currentTarget.closest(".edit-list");
+        for (const child of list.children) {
+            child.style.borderTop = "";
+            child.style.borderBottom = "";
+        }
+        var sourceElement = list.children[sourceIndex];
+        if (sourceElement !== event.currentTarget) {
+            sourceElement.remove();
+            list.insertBefore(sourceElement, event.currentTarget);
+        }
+    }
+    </script>
+</div>
+{%- endif %}
+
+{%- endblock %}
index 37f6ba0037003008c10cf7f495ed7acf0bb650ab..f9ae7f50f89414d192293c4104b087eb3bd409e5 100644 (file)
         <label for="public">Public</label><br>
 
         <a href="javascript:hideAddPlaylist();">Cancel</a>
-        <input type="submit" value="Create Playlist" />
+        <input type="submit" value="Create Playlist" style="margin: 10px;"/>
     </form>
     <script>
         function showAddPlaylist() {
index f587faefce48c0ecdd2fc1319c1cdf04506cc2f4..3ec44405e5c31051389536f6d920fc9d5b6d962b 100644 (file)
@@ -32,7 +32,7 @@
             <div class="song-buttons">
 
                 <!-- Owner-Specific Buttons (Edit/Delete) -->
-                {% if session["userid"] == song.userid %}
+                {% 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>
             {% if current_user_playlists %}
             <!-- Add to Playlist Buttons -->
             <div class="song-playlist-controls">
-                <button type="button" class="button" onclick="return showPlaylistSelector(event, {{ song.songid }})">Add to Playlist</button>
+                <form action="/append-to-playlist" method="post">
+                    <input type="hidden" name="songid" value="{{ song.songid }}" id="playlist-selector-songid"/>
+                    <select name="playlistid">
+                        <option value="-1">Add to Playlist...</option>
+                        {% for plist in current_user_playlists -%}
+                        <option value="{{ plist.playlistid }}" onclick="this.closest('form').submit()">{{ plist['name'] }}</option>
+                        {%- endfor %}
+                    </select>
+                </form>
             </div>
             {% endif %}
 
         </div>
     </div>
 {% endfor %}
-
-    {% if current_user_playlists -%}
-    <!-- Playlist selector, shown when Add to Playlist is clicked -->
-    <div class="playlist-selector" hidden>
-        <form action="/append-to-playlist" method="post">
-            <input type="hidden" name="songid" value="-1" id="playlist-selector-songid"/>
-            <select name="playlistid">
-                {% for plist in current_user_playlists -%}
-                <option value="{{ plist.playlistid }}">{{ plist['name'] }}</option>
-                {%- endfor %}
-            </select>
-            <input type="submit" value="submit" />
-        </form>
-    </div>
-    {%- endif %}
 </div>
 
 <script>
 function showDetails(event) {
     var songElement = event.target.closest(".song");
-
-    for (const child of songElement.children) {
-        if (child.classList.contains("song-details")) {
-            if (child.hidden) {
-                // Show details
-                child.hidden = false;
-                event.target.alt = "Hide Details";
-                event.target.className = "lsp_btn_hide02";
-                event.target.src = customImage(document.getElementById("lsp_btn_hide02"));
-            }
-            else {
-                // Hide details
-                child.hidden = true;
-                event.target.alt = "Show Details";
-                event.target.className = "lsp_btn_show02";
-                event.target.src = customImage(document.getElementById("lsp_btn_show02"));
-            }
-        }
+    var songDetails = songElement.querySelector(".song-details");
+    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"));
+    }
+    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"));
     }
     return false;
 }
-
-{% if current_user_playlists %}
-var m_addToPlaylistSongid = null;
-function showPlaylistSelector(event, songid) {
-    m_addToPlaylistSongid = songid;
-    var songList = event.target.closest(".song-list");
-    var playlistSelector = songList.querySelector(".playlist-selector");
-    playlistSelector.hidden = false;
-    var songidInput = playlistSelector.querySelector("#playlist-selector-songid")
-    songidInput.value = songid
-    return false;
-}
-{% endif %}
 </script>