diff --git a/dist/stagingarea.html b/dist/stagingarea.html
index 734d0f2..093396d 100644
--- a/dist/stagingarea.html
+++ b/dist/stagingarea.html
@@ -25,14 +25,18 @@
-
diff --git a/dist/stagingarea.js b/dist/stagingarea.js
index 3a37945..d227d12 100644
--- a/dist/stagingarea.js
+++ b/dist/stagingarea.js
@@ -19,6 +19,106 @@ $(document).ready(async function () {
Create a staging card for a song
Params - name {string}, artists {string}, imgUrl {string}
*/
+
+ // Remove songs from a playlist
+ let removeAction = document.getElementById('removeAction');
+ removeAction.addEventListener('click', async function() {
+ let data = JSON.parse(localStorage.getItem(user_id));
+ let songs = Object.keys(data[playlist_id]['left_tracks']);
+
+ let removeSongs = confirm(`Are you sure you want to remove ${songs.length} songs?`);
+ if (removeSongs) {
+ let access_token = localStorage.getItem('access_token');
+ if (checkAccessTokenExpiration()) access_token = refreshAccessToken();
+ if (access_token == null) renderError('Error refreshing access token.');
+
+ let params = new URLSearchParams();
+ params.set('playlist_id', playlist_id);
+
+ let data = { to_remove: songs };
+ let res = await fetch(`${API_URI}/playlist/remove?${params.toString()}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': access_token,
+ 'Access-Control-Allow-Origin': '*',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data)
+ });
+
+ if (res.status != 200) {
+ alert('Error removing songs');
+ } else {
+ alert('Songs successfully removed!');
+ }
+
+ }
+
+ data[playlist_id]['left_tracks'] = {};
+ save_state = data[playlist_id];
+ localStorage.setItem(user_id, JSON.stringify(data));
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+ generateCard('left_tracks');
+ });
+
+
+ let createAction = document.getElementById('createAction');
+ createAction.addEventListener('click', async function() {
+ let data = JSON.parse(localStorage.getItem(user_id));
+ let songs = Object.keys(data[playlist_id]['right_tracks']);
+
+ let playlistName = prompt(`Enter a name for a new playlist with ${songs.length} songs:`);
+ if (playlistName != "" && playlistName != null) {
+ let access_token = localStorage.getItem('access_token');
+ if (checkAccessTokenExpiration()) access_token = refreshAccessToken();
+ if (access_token == null) renderError('Error refreshing access token.');
+
+ let data = {
+ name: playlistName,
+ description: 'Created with filtered songs from SongSwipe! https://github.com/ListenToAJ/SongSwipe',
+ is_public: true,
+ };
+
+ let createRes = await fetch(`${API_URI}/playlist/create`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': access_token,
+ 'Access-Control-Allow-Origin': '*',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data)
+ });
+
+ if (createRes.status != 201) {
+ alert('Error creating playlist!');
+ }
+
+ let newPlaylist = await createRes.json();
+
+ let params = new URLSearchParams();
+ params.set('playlist_id', newPlaylist.id);
+
+ data = { to_add: songs };
+ let addRes = await fetch(`${API_URI}/playlist/add?${params.toString()}`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': access_token,
+ 'Access-Control-Allow-Origin': '*',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data)
+ });
+
+ if (addRes.status != 201) {
+ alert('Error adding songs to new playlist!');
+ } else {
+ alert('Created new playlist with songs.');
+ }
+ }
+ })
+
let container = document.getElementById('stageContainer');
function createStagingCard(track, trackId) {
let card = document.createElement('div');
diff --git a/dist/style.css b/dist/style.css
index 334cc28..36bf281 100644
--- a/dist/style.css
+++ b/dist/style.css
@@ -114,6 +114,12 @@ body{
position: relative;
}
+#header_container_staging{
+ width: 100%;
+ height: 600px;
+ position: relative;
+}
+
#playlist_title{
color: white;
font-size: 3vh;
@@ -359,28 +365,84 @@ transform: translateX(200px) rotate(20deg);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
-.button_container button:hover {
- transform: scale(1.1);
- background-color: rgba(20, 25, 29, 1);
+/* Remove the content:url approach entirely */
+.song_button {
+ position: relative;
+}
+
+.song_button::before,
+.song_button::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 40%; /* Adjust as needed */
+ height: 40%; /* Adjust as needed */
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
}
-/* Individual Button Icons */
.song_button::before {
- content: "▶";
- font-size: 26px;
+ background-image: url(./assets/img/play.png);
+ opacity: 1;
+}
+
+.song_button::after {
+ background-image: url(./assets/img/pause.png);
+ opacity: 0;
}
.song_button.playing::before {
- content: "⏸";
- font-size: 40px;
- padding-top: 5px;
+ opacity: 0;
+}
+
+.song_button.playing::after {
+ opacity: 1;
+}
+
+.button_container button:hover {
+ transform: scale(1.1);
+ background-color: rgba(20, 25, 29, 1);
+}
+
+/* Song restart button */
+.song_restart {
+ position: relative;
}
.song_restart::before {
- content: "⟳";
- font-size: 60px;
- padding-bottom: 7px;
- padding-left: 6px;
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 40%; /* Adjust as needed */
+ height: 40%; /* Adjust as needed */
+ background-image: url(./assets/img/restart.png);
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+/* Song restart button */
+.stage_area {
+ position: relative;
+}
+
+.stage_area::before {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 40%; /* Adjust as needed */
+ height: 40%; /* Adjust as needed */
+ background-image: url(./assets/img/stage.png);
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
}
/* Go Back Button Styling */
@@ -401,7 +463,7 @@ transform: translateX(200px) rotate(20deg);
transition: all 0.2s ease;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
z-index: 300; /* Ensure it's above other elements */
- font-size: 26px;
+ /* position: relative; Add this for proper positioning of the ::before element */
}
.back_button:hover {
@@ -413,13 +475,23 @@ transform: translateX(200px) rotate(20deg);
transform: scale(0.95);
}
-/* Back arrow icon */
+/* Back arrow icon - modified to use the image */
.back_button::before {
- content: "←";
- font-size: 26px;
- font-weight: bold;
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 40%; /* Adjust as needed */
+ height: 40%; /* Adjust as needed */
+ background-image: url(./assets/img/back.png);
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
}
+
+
/* LANDING PAGEEE */
#landing_header{
@@ -560,7 +632,7 @@ transform: translateX(200px) rotate(20deg);
height: auto;
width: 100%;
position: absolute;
- top: calc(180px - 5%);
+ top: calc(260px - 5%);
text-align: center;
}
@@ -688,6 +760,17 @@ transform: translateX(200px) rotate(20deg);
padding-right: 2px;
}
+.action {
+ color: rgba(255, 255, 255, 0.7);
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(1px);
+}
+
+.action:hover {
+ color: rgba(0, 0, 0, 1);
+ background: rgba(255, 255, 255, 0.5);
+ backdrop-filter: blur(1px);
+}
.selected {
color: rgba(0, 0, 0, 1);
@@ -704,4 +787,187 @@ transform: translateX(200px) rotate(20deg);
.status-text {
margin-top: 20px;
font-size: 16px;
+}
+
+.slider-container {
+ border: 2px solid #FAF9F6;
+ display: flex;
+ /* Fixed positioning relative to viewport height */
+ position: absolute;
+ left: 50%;
+ top: 72.5vh; /* Center vertically at 50% of viewport height */
+ transform: translate(-50%, -50%); /* Center both horizontally and vertically */
+ align-items: center;
+ background-color: rgba(20, 25, 29, 0.8);
+ border-radius: 50px;
+ padding: 20px 30px;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+ width: 80%;
+ height: 2%;
+ margin: 50px auto;
+ gap: 20px;
+ z-index: 500;
+}
+
+.slider-track {
+ position: relative;
+ height: 8px;
+ background-color: #EEEEEE;
+ border-radius: 3px;
+ flex-grow: 1;
+ z-index: 200;
+}
+
+.slider-progress {
+ position: absolute;
+ height: 100%;
+ background-color: #00C8E6;
+ border-radius: 3px;
+ width: 70%;
+}
+
+.slider-handle {
+ position: absolute;
+ width: 2vh;
+ height: 2vh;
+ background-color: white;
+ border-radius: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ left: 70%;
+ cursor: pointer;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+ border: 2px solid #00C8E6;
+}
+
+.slider-value {
+ font-family: Arial, sans-serif;
+ font-size: 18px;
+ font-weight: bold;
+ color: #ffffff;
+ min-width: 30px;
+ text-align: right;
+}
+
+/* Go Back Button Styling */
+.question_button {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 8vh;
+ height: 8vh;
+ border-radius: 50%;
+ border: 2px solid #FAF9F6;
+ background-color: rgba(20, 25, 29, 0.8);
+ color: #FAF9F6;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: all 0.2s ease;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ z-index: 300; /* Ensure it's above other elements */
+ /* position: relative; Add this for proper positioning of the ::before element */
+}
+
+.question_button:hover {
+ transform: scale(1.1);
+ background-color: rgba(20, 25, 29, 1);
+}
+
+.question_button:active {
+ transform: scale(0.95);
+}
+
+.question_button::before {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 40%; /* Adjust as needed */
+ height: 40%; /* Adjust as needed */
+ background-image: url(./assets/img/question.png);
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+/* When User is confused */
+#question {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgb(32,32,32);
+ /* padding-top: env(safe-area-inset-top); */
+ background-image: url('assets/img/background.jpg');
+ background-size: cover; /* Ensures the image covers the whole screen */
+ background-position: center; /* Centers the image */
+ background-repeat: no-repeat; /* Prevents tiling */
+ overscroll-behavior-x: none;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.question-content {
+ /* background-color: white; */
+ /* padding: 20px; */
+ border-radius: 5px;
+ max-width: 500px;
+ text-align: center;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ z-index: 1000;
+}
+
+#question.hidden {
+ display: none;
+}
+
+#close-question {
+ position: absolute;
+ z-index: 1001;
+ margin-top: 15px;
+ bottom: 5vh;
+ padding: 28px 46px;
+ background-color: #14191d;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ margin: auto;
+ left: 0px;
+ right: 0px;
+ width: 50%;
+ font-size: 2.7vh;
+ letter-spacing: -0.01em;
+ font-family: 'Gotham' !important;
+ border: 2px solid;
+}
+
+#close-question.hidden {
+ display: none;
+}
+
+#question_text {
+ color: white;
+ font-size: 2.8vh;
+ top: 400px;
+ transform: translate(-50%, -50%); /* Center both horizontally and vertically */
+ margin: auto;
+ display: flex;
+ position: absolute;
+ width: 90%;
+ max-width: 600px;
+ z-index: 1000;
+ font-family: 'DM Serif Display';
+ text-align: left;
+ padding: 15px;
+ line-height: 2.2; /* Increased line height for spacing */
+ white-space: pre-line; /* Preserves new lines from the text */
}
\ No newline at end of file
diff --git a/dist/util.js b/dist/util.js
index 77b9b76..dadbc45 100644
--- a/dist/util.js
+++ b/dist/util.js
@@ -169,4 +169,62 @@ async function sendElapsedTime(playlist_id, user_id, total_time) {
const response = await fetch(request);
response.status;
-}
\ No newline at end of file
+}
+
+$(document).ready(async function () {
+ // Get the current URL
+ const currentURL = window.location.href;
+ // Get just the pathname (everything after the domain)
+ const pathname = window.location.pathname;
+ // Get just the filename
+ const filename = pathname.split('/').pop();
+ // Question Mark Message
+ let question_message = "YAP";
+ let page_status = null;
+ if (pathname.includes('index.html')) {
+ question_message = "YIPEE";
+ page_status = 1;
+ } else if (pathname.includes('cards.html')) {
+ question_message = `
+ PLAYLIST CONTROLS
+ ----------------
+ - Swipe Left - REMOVE song from playlist
+ - Swipe Right - KEEP song in playlist
+ - Play Button - Play the song
+ - Pause Button - Pause the song
+ - Restart Button - Restart the preview
+ - Back Arrow - Return to playlist library
+ - Menu (3 lines) - Go to staging area
+ `;
+ page_status = 2;
+ } else if (pathname.includes('playlists.html')) {
+ question_message = "rah";
+ page_status = 3;
+ } else if (pathname.includes('stagingarea.html')) {
+ question_message = "ney";
+ page_status = 4;
+ return; // THIS WILL PROBABLY BE REMOVED!
+ }
+// Question mark handling
+ // Hide Question UI Stuff Until user pressed button
+ const Question = document.getElementById('question');
+ const closeButton = document.getElementById('close-question');
+ questionText = document.getElementById('question_text');
+ questionText.innerHTML = question_message;
+ Question.classList.add('hidden');
+
+ $(".question_button").click(function () {
+ Question.classList.remove('hidden');
+ if (page_status == 2) {
+ pauseSong();
+ }
+ });
+
+ // Close overlay when button is clicked
+ closeButton.addEventListener('click', async function () {
+ Question.classList.add('hidden');
+ if (page_status == 2) {
+ playSong();
+ }
+ });
+});
\ No newline at end of file
diff --git a/package.json b/package.json
index 880095f..08e8b5b 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"start": "npx tsc && ./node_modules/.bin/netlify-lambda serve build",
"dev": "npx tsc && concurrently \"http-server ./dist/\" \"./node_modules/.bin/netlify-lambda serve build\"",
"build": "npx tsc && ./node_modules/.bin/netlify-lambda build build",
+ "windows": "npx tsc && concurrently \"http-server ./dist/\" \".\\node_modules\\.bin\\netlify-lambda.cmd serve build\"",
"test": "jest"
},
"dependencies": {
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..7e4225d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+matplotlib
+pandas
+numpy
\ No newline at end of file
diff --git a/spotify_data_collection.py b/spotify_data_collection.py
new file mode 100644
index 0000000..0642828
--- /dev/null
+++ b/spotify_data_collection.py
@@ -0,0 +1,37 @@
+"""
+Python script to help with Spotify data collection.
+Will have a prompt that lets you select left or right for each song and will keep track of the time
+Writes to a csv in the data folder with the name 'data/{name}_{playlist}_spotify.csv'
+It is recomended that you rename the auto generated files from the app to the same scheme so its not just random strings.
+"""
+import time
+import csv
+
+name = input('Enter a name: ')
+playlist = input('Enter a playlist name: ')
+
+with open(f'data/{name}_{playlist}_spotify.csv', 'w', newline='') as f:
+ song_num = 1
+ writer = csv.writer(f)
+ writer.writerow(['song_num', 'time(Sec)', 'decision'])
+
+ while True:
+ # Data collecting
+ start_time = time.time()
+ decision = input('Enter one of the following characters: l (left), r (right), d (done): ')
+ while decision not in ['l', 'r', 'd']:
+ decision = input('Enter one of the following characters: l (left), r (right), d (done): ')
+
+ # change decision variable to be full word, here so you dont have to type the whole word out
+ if decision == 'd': break
+ elif decision == 'l': decision = 'left'
+ elif decision == 'r': decision = 'right'
+
+ decision_time = round(time.time() - start_time)
+
+ print(f'Song {song_num}, Decision {decision}, Time {decision_time}')
+
+ writer.writerow([song_num, decision_time, decision])
+ song_num += 1
+
+ print(f'Testing completed, {song_num} songs made decisions on.')