@@ -583,20 +623,59 @@
Playlist Import Machine
+
+
+
+
+
+
+ Edit the song tag + hit enter to save
+
+ Rohn Standard Notation
+ Standard: Artist - Song Title
+ Remix: Artist - Song Title (Remixartist Remix)
+ Featuring: Artist - Song Title feat. Subartist
+ Featuring + Remix: Artist - Song Title feat. Subartist (Remixartist Remix)
+
+
+
-
+
+
+
+
+
diff --git a/js/chat.js b/js/chat.js
index 4e48484..e89f69c 100644
--- a/js/chat.js
+++ b/js/chat.js
@@ -187,7 +187,7 @@ firetable.ui.setupChatEvents = function () {
// ── New message block (different user or @-mention break) ──
var $chatthing = $chatTemplate.clone();
$chatthing.attr('id', "chat" + chatData.chatID);
- $chatthing.find('.botson').css(
+ $chatthing.find('.ft-avatar').css(
'background-image',
"url(" + firetable.utilities.avatarURL(chatData.id, namebo) + ")"
);
@@ -201,7 +201,7 @@ firetable.ui.setupChatEvents = function () {
$chatthing.find(".chatName").text(namebo);
// Click-to-@ on avatar and name
- firetable.utilities.chatAt($chatthing.find('.botson'));
+ firetable.utilities.chatAt($chatthing.find('.ft-avatar'));
firetable.utilities.chatAt($chatthing.find('.chatName'));
twemoji.parse($chatthing.find(".chatText")[0]);
$chatthing.appendTo("#chats");
diff --git a/js/firetable.js b/js/firetable.js
index 6a5dc04..91e65ce 100644
--- a/js/firetable.js
+++ b/js/firetable.js
@@ -7,6 +7,7 @@ var ftapi = {
started: false,
loggedIn: false,
banned: false,
+ isMod: false,
presenceDetectRef: null,
presenceDetectEvent: null,
blockEvent: null,
@@ -127,6 +128,21 @@ ftapi.init = function(firebaseConfig) {
ftapi.events.emit("newHistory", data);
});
+ recentz.on('child_changed', function(dataSnapshot) {
+ var data = dataSnapshot.val();
+ data.histID = dataSnapshot.key;
+ ftapi.events.emit("editedHistory", data);
+ });
+
+ // new songs emitter
+ var freshproduce = firebase.app("firetable").database().ref("newMusic");
+ freshproduce.on('child_added', function(dataSnapshot, prev) {
+ var data = dataSnapshot.val();
+ data.histID = dataSnapshot.key;
+ ftapi.events.emit("newProduce", data);
+ });
+
+
// table change emitter
var tbl = firebase.app("firetable").database().ref("table");
tbl.on('value', function(dataSnapshot) {
@@ -221,6 +237,10 @@ ftapi.init = function(firebaseConfig) {
firetable.avatarStyle = data.avatarStyle;
localStorage[STORAGE.avatarStyle] = data.avatarStyle;
}
+ if (data.mod || data.supermod) {
+ ftapi.isMod = true;
+ ftapi.events.emit("modCheck", true);
+ }
}
ftapi.events.emit("loggedIn", returnData);
@@ -516,42 +536,6 @@ ftapi.actions = {
var removeThis = ftapi.queueRef.child(trackID + "/flagged");
removeThis.remove();
},
- editTrackTag: function(trackID, cid, newTag) {
- if (ftapi.queue[trackID]) {
- if (ftapi.queue[trackID].cid == cid) {
- var changeref = ftapi.queueRef.child(trackID);
- var trackObj = ftapi.queue[trackID];
- trackObj.name = newTag;
- changeref.set(trackObj);
- } else {
- //song appears to have moved since the editing began, let's try and find it...
- for (var key in ftapi.queue) {
- if (ftapi.queue.hasOwnProperty(key)) {
- if (ftapi.queue[key].cid == cid) {
- var changeref = ftapi.queueRef.child(key);
- var trackObj = ftapi.queue[key];
- trackObj.name = newTag;
- changeref.set(trackObj);
- return;
- }
- }
- }
- }
- } else {
- //song appears to have moved since the editing began, let's try and find it...
- for (var key in ftapi.queue) {
- if (ftapi.queue.hasOwnProperty(key)) {
- if (ftapi.queue[key].cid == cid) {
- var changeref = ftapi.queueRef.child(key);
- var trackObj = ftapi.queue[key];
- trackObj.name = newTag;
- changeref.set(trackObj);
- return;
- }
- }
- }
- }
- },
deleteList: function(listID) {
var removeThis = firebase.app("firetable").database().ref("playlists/" + ftapi.uid + "/" + listID);
removeThis.remove();
@@ -790,6 +774,20 @@ ftapi.actions = {
deleteChat: function(feedID) {
var feedEntry = firebase.app("firetable").database().ref("chatFeed/" + feedID + "/hidden");
feedEntry.set(true);
+ },
+ editTag: function(type, cid, tag, histID){
+ console.log(type+" "+cid+" "+tag+ " "+histID);
+ var yargo = tag.split(" - ");
+ var theartist = yargo[0];
+ var thetitle = yargo[1];
+ //UPDATE HISTORY
+ var histRef = firebase.app("firetable").database().ref("songHistory/" + histID);
+ histRef.child("artist").set(theartist);
+ histRef.child("title").set(thetitle);
+ //UPDATE GLOBAL
+ var globalRef = firebase.app("firetable").database().ref("globalTracks/" + type + "" +cid);
+ globalRef.child("artist").set(theartist);
+ globalRef.child("title").set(thetitle);
}
};
@@ -1000,4 +998,4 @@ EventEmitter.prototype.once = function(event, listener) {
});
};
-ftapi.ready();
+ftapi.ready();
\ No newline at end of file
diff --git a/js/helpers.js b/js/helpers.js
index 052efb5..160bc37 100644
--- a/js/helpers.js
+++ b/js/helpers.js
@@ -152,6 +152,11 @@ firetable.utilities = {
$('body').addClass('screen');
},
+ /** Returns "count singular" or "count plural" (defaults to singular+"s") */
+ pluralize: function (count, singular, plural) {
+ return count + " " + (count === 1 ? singular : (plural || singular + "s"));
+ },
+
// ─── Chat Scroll ─────────────────────────────────────────────────────────
/**
@@ -256,7 +261,7 @@ firetable.utilities = {
/**
* Attach a click handler to an element that inserts "@username " into the chat input.
- * Works on .prson (user list), .botson (chat avatar), and .chatName elements.
+ * Works on .prson (user list), .ft-avatar (chat avatar), and .chatName elements.
* @param {jQuery} element - jQuery-wrapped DOM element
*/
chatAt: function (element) {
@@ -264,7 +269,7 @@ firetable.utilities = {
var nameToAt;
if (element.hasClass("prson")) {
nameToAt = $(this).find(".prsnName").text();
- } else if (element.hasClass("botson")) {
+ } else if (element.hasClass("ft-avatar")) {
nameToAt = $(this).next(".chatContent").find(".chatName").text();
} else if (element.hasClass("chatName")) {
nameToAt = $(this).text();
diff --git a/js/init.js b/js/init.js
index 964e899..d3ad859 100644
--- a/js/init.js
+++ b/js/init.js
@@ -49,13 +49,25 @@ firetable.init = function () {
{ cls: "soundcloud", url: ftconfigs.soundcloudURL }
];
socialLinks.forEach(function (link) {
- if (link.url) {
- $(".sociallogo." + link.cls)
- .attr("href", link.url)
- .css("display", "inline-block");
- }
+ if (link.url) $(".sociallogo." + link.cls).attr("href", link.url);
});
+ // Social popover: position anchored to trigger, swap icon on toggle
+ var socialPopoverEl = document.getElementById('socialPopover');
+ if (socialPopoverEl) {
+ socialPopoverEl.addEventListener('toggle', function (e) {
+ var btn = document.getElementById('socialTrigger');
+ var icon = btn && btn.querySelector('.material-icons');
+ if (e.newState === 'open') {
+ if (icon) icon.textContent = 'close';
+ socialPopoverEl.style.visibility = 'hidden';
+ firetable.ui.positionPopover(btn, socialPopoverEl, document.getElementById('socialArrow'));
+ } else {
+ if (icon) icon.textContent = 'share';
+ }
+ });
+ }
+
if (ftconfigs.logoImage) {
$("#roomlogo").css("background-image", "url(" + ftconfigs.logoImage + ")");
}
@@ -75,10 +87,6 @@ firetable.init = function () {
$('#playerArea, #scScreen')
.width($('#djStage').outerWidth())
.height($('#djStage').outerHeight());
- $("#stealContain").css({
- 'top': $('#grab').offset().top + $('#grab').height(),
- 'left': $('#grab').offset().left - 16
- });
setup(); // Re-create the p5.js canvas at the new size
}, 500));
@@ -117,7 +125,6 @@ firetable.init = function () {
// ── DOM Templates ──
$playlistItemTemplate = $('#mainqueue .pvbar').remove();
- $tagEditorTemplate = $('.tagPromptBox').remove();
// ── Firebase Init ──
ftapi.init(ftconfigs.firebase);
diff --git a/js/playlist.js b/js/playlist.js
index dccc375..d6e040e 100644
--- a/js/playlist.js
+++ b/js/playlist.js
@@ -351,7 +351,7 @@ firetable.ui.dubtrackImportFileSelect = function (evt) {
});
}
if (firetable.dtImportList.length) {
- $("#importDubResults").text("Ok... import " + firetable.dtImportName + " (" + firetable.dtImportList.length + " tracks)?");
+ $("#importDubResults").text("Ok... import " + firetable.dtImportName + " (" + firetable.utilities.pluralize(firetable.dtImportList.length, "track") + ")?");
$("#dubimportButton").show();
} else {
$("#importDubResults").text("ERROR... NO TRAX?");
@@ -372,13 +372,19 @@ firetable.ui.dubtrackImportFileSelect = function (evt) {
* @param {string} songid - data-key of the history item
* @param {string} tag - Current "Artist - Title" string
*/
-firetable.actions.editTagsPrompt = function (songid, tag) {
- var $pvbar = $('#thehistory .pvbar[data-key="' + songid + '"]');
- $('#thehistory .pvbar.editing').removeClass('editing');
- $('.tagPromptBox').remove();
+firetable.actions.editTagsPrompt = function (songid, tag, anchorEl) {
+ var popoverEl = document.getElementById('tagEditorPopover');
+ if (popoverEl.matches(':popover-open')) popoverEl.hidePopover();
+ $('.pvbar.editing').removeClass('editing');
+ var $pvbar = $('.pvbar[data-key="' + songid + '"]').first();
$pvbar.addClass('editing');
- var $tags = $tagEditorTemplate.clone().appendTo($pvbar);
- $tags.find(".tagMachine").val(tag);
+ firetable.editingPvbar = $pvbar;
+ $(popoverEl).find('.tagMachine').val(tag);
+ popoverEl.style.visibility = 'hidden';
+ popoverEl.showPopover();
+ firetable.ui.positionPopover(anchorEl || $pvbar.find('.edittags')[0], popoverEl, document.getElementById('tagEditorArrow'), 'bottom').then(function () {
+ $(popoverEl).find('.tagMachine')[0].focus();
+ });
firetable.debug && console.log('edit tags song id:', songid);
};
@@ -417,6 +423,12 @@ firetable.ui.setupPlaylistEvents = function () {
.attr("data-type", thisone.type)
.attr("data-cid", thisone.cid);
+ // Album art thumbnail
+ var artUrl = thisone.type === MEDIA_YOUTUBE
+ ? 'https://i.ytimg.com/vi/' + thisone.cid + '/mqdefault.jpg'
+ : '';
+ if (artUrl) $newli.find('.q-art').css('background-image', 'url(' + artUrl + ')');
+
// Preview button
$newli.find('.previewicon').attr('id', "pv" + key).on('click', function () {
firetable.actions.pview(
@@ -474,6 +486,28 @@ firetable.ui.setupPlaylistEvents = function () {
firetable.actions.deleteSong($(this).closest('.pvbar').attr('data-key'));
});
+ // Edit tags button
+ $newli.find('.edittags').on('click', function () {
+ var popoverEl = document.getElementById('tagEditorPopover');
+ var $pvbar = $(this).closest('.pvbar');
+ if (popoverEl.matches(':popover-open') && firetable.editingPvbar && firetable.editingPvbar.is($pvbar)) {
+ popoverEl.hidePopover();
+ } else {
+ firetable.actions.editTagsPrompt(
+ $pvbar.attr('data-key'),
+ $pvbar.find('.listwords').text(),
+ this
+ );
+ }
+ });
+
+ // Close editor button
+ $newli.find('.closeeditor').on('click', function () {
+ document.getElementById('tagEditorPopover').hidePopover();
+ });
+
+ if (!ftapi.isMod) $newli.find('.edittags, .closeeditor').hide();
+
// Add-to-playlist button
$newli.find('.histeal').on('click', function () {
var $btn = $(this);
@@ -507,10 +541,10 @@ firetable.ui.setupPlaylistEvents = function () {
firetable.stealSourceBtn = $btn;
firetable.stealTarget = { cid: btnCid, type: btnType, title: btnTitle };
$btn.addClass('on');
- $("#stealContain").css({
- top: $btn.offset().top + $btn.outerHeight(),
- left: $btn.offset().left - 16
- }).show();
+ var stealContainEl = document.getElementById('stealContain');
+ stealContainEl.style.visibility = 'hidden';
+ $("#stealContain").show();
+ firetable.ui.positionPopover($btn[0], stealContainEl, document.getElementById('stealArrow'), 'left');
});
});
@@ -641,8 +675,8 @@ firetable.ui.setupPlaylistEvents = function () {
// ── Tag editing (Enter in .tagMachine) ──
$(document).on("keyup", ".tagMachine", function (e) {
if (e.which !== 13) return;
- var songCid = $(this).closest('.pvbar').attr('data-cid');
- var histID = $(this).closest('.pvbar').attr('data-histid');
+ var $pvbar = firetable.editingPvbar;
+ if (!$pvbar || !$pvbar.length) return;
var val = $(this).val();
if (!val) return;
var yargo = val.split(" - ");
@@ -650,10 +684,20 @@ firetable.ui.setupPlaylistEvents = function () {
alert("check yr tags");
} else {
ftapi.actions.editTag(
- $(this).closest('.pvbar').attr('data-type'),
- songCid, val, histID
+ $pvbar.attr('data-type'),
+ $pvbar.attr('data-cid'),
+ val,
+ $pvbar.attr('data-histid')
);
- $(this).closest('.editing').removeClass('editing').next('.tagPromptBox').remove();
+ document.getElementById('tagEditorPopover').hidePopover();
+ }
+ });
+
+ // ── Tag editor popover cleanup on auto-dismiss ──
+ document.getElementById('tagEditorPopover').addEventListener('toggle', function (e) {
+ if (e.newState === 'closed' && firetable.editingPvbar) {
+ firetable.editingPvbar.removeClass('editing');
+ firetable.editingPvbar = null;
}
});
};
diff --git a/js/popover.js b/js/popover.js
new file mode 100644
index 0000000..b5c9204
--- /dev/null
+++ b/js/popover.js
@@ -0,0 +1,35 @@
+// ─── Floating UI Popover Positioning ────────────────────────────────────────
+// Shared utility: positions a floating element anchored to a reference element.
+// arrowEl is optional (pass null for no arrow).
+
+firetable.ui.positionPopover = function (anchorEl, floatingEl, arrowEl) {
+ var middleware = [
+ FloatingUIDOM.offset(8),
+ FloatingUIDOM.flip(),
+ FloatingUIDOM.shift({ padding: 8 })
+ ];
+ if (arrowEl) {
+ middleware.push(FloatingUIDOM.arrow({ element: arrowEl }));
+ }
+ FloatingUIDOM.computePosition(anchorEl, floatingEl, {
+ placement: 'bottom-start',
+ strategy: 'fixed',
+ middleware: middleware
+ }).then(function (pos) {
+ floatingEl.style.left = pos.x + 'px';
+ floatingEl.style.top = pos.y + 'px';
+
+ if (arrowEl && pos.middlewareData.arrow) {
+ var ax = pos.middlewareData.arrow.x;
+ var ay = pos.middlewareData.arrow.y;
+ var staticSide = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[pos.placement.split('-')[0]];
+ Object.assign(arrowEl.style, {
+ left: ax != null ? ax + 'px' : '',
+ top: ay != null ? ay + 'px' : '',
+ right: '',
+ bottom: '',
+ [staticSide]: '-4px'
+ });
+ }
+ });
+};
diff --git a/js/room.js b/js/room.js
index e069095..0d13b5b 100644
--- a/js/room.js
+++ b/js/room.js
@@ -90,22 +90,20 @@ function renderHistoryItem(data, $template, containerSel, artClass) {
});
// Track link
- $histItem.find('.histlink').attr({
- 'href': data.url,
- 'tabindex': "-1",
- 'id': data.histID
- }).text(data.artist + " - " + data.title);
+ $histItem.find('.histlink').attr('id', data.histID).text(data.artist + " - " + data.title);
+ $histItem.find('.tracklink-btn').attr('href', data.url || '');
// Edit tags button (mod only)
$histItem.find('.edittags').on('click', function () {
- if ($(this).hasClass("editing")) {
- $(this).removeClass("editing");
- $(this).closest('.pvbar').find('.tagPromptBox').remove();
+ var popoverEl = document.getElementById('tagEditorPopover');
+ var $pvbar = $(this).closest('.pvbar');
+ if (popoverEl.matches(':popover-open') && firetable.editingPvbar && firetable.editingPvbar.is($pvbar)) {
+ popoverEl.hidePopover();
} else {
- $(this).addClass("editing");
firetable.actions.editTagsPrompt(
- $(this).closest('.pvbar').attr('data-key'),
- data.artist + " - " + data.title
+ $pvbar.attr('data-key'),
+ data.artist + " - " + data.title,
+ this
);
}
});
@@ -117,6 +115,9 @@ function renderHistoryItem(data, $template, containerSel, artClass) {
// Metadata
$histItem.find('.histdj').text(data.dj);
+ $histItem.find('.hist-dj-avatar')
+ .css('background-image', 'url(' + firetable.utilities.avatarURL(data.djid || data.dj, data.dj, '40x40') + ')')
+ .attr('data-label', data.dj);
$histItem.find('.histdate').text(firetable.utilities.format_date(data.when));
$histItem.find('.histtime').text(firetable.utilities.format_time(data.when));
@@ -156,10 +157,10 @@ function renderHistoryItem(data, $template, containerSel, artClass) {
firetable.stealTarget = { cid: btnCid, type: btnType, title: btnTitle };
$btn.addClass('on');
- $("#stealContain").css({
- top: $btn.offset().top + $btn.outerHeight(),
- left: $btn.offset().left - 16
- }).show();
+ var stealContainEl = document.getElementById('stealContain');
+ stealContainEl.style.visibility = 'hidden';
+ $("#stealContain").show();
+ firetable.ui.positionPopover($btn[0], stealContainEl, document.getElementById('stealArrow'), 'left');
});
});
@@ -168,7 +169,30 @@ function renderHistoryItem(data, $template, containerSel, artClass) {
$histItem.find('.' + artClass).css('background-image', 'url(' + data.img + ')');
}
- $histItem.prependTo(containerSel);
+ if (containerSel === "#thehistory") {
+ var dateKey = firetable.utilities.format_date(data.when);
+ var when = new Date(data.when);
+ var dateLabel = when.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
+ var $dayGroup = $('#thehistory .hist-day-group[data-date="' + dateKey + '"]');
+ if ($dayGroup.length === 0) {
+ $dayGroup = $(
+ '
'
+ );
+ $dayGroup.prependTo('#thehistory');
+ }
+ var timeStr = firetable.utilities.format_time(data.when);
+ var $avatar = $histItem.find('.hist-dj-avatar').detach();
+ var $entry = $('
');
+ $('
' + timeStr + '').appendTo($entry);
+ $avatar.appendTo($entry);
+ $histItem.appendTo($entry);
+ $entry.prependTo($dayGroup.find('.hist-day-items'));
+ } else {
+ $histItem.prependTo(containerSel);
+ }
}
// ─── Room Event Binding ──────────────────────────────────────────────────────
@@ -190,14 +214,23 @@ firetable.ui.setupRoomEvents = function () {
function applyHistoryFilter() {
var q = ($("#histFilter").val() || "").toLowerCase().trim();
- $("#thehistory .pvbar").each(function () {
- var text = ($(this).find('.histlink').text() + " " + $(this).find('.histdj').text()).toLowerCase();
+ $("#thehistory .hist-entry").each(function () {
+ var $pvbar = $(this).find('.pvbar');
+ var text = ($pvbar.find('.histlink').text() + " " + $pvbar.find('.histdj').text()).toLowerCase();
$(this).toggle(q.length === 0 || text.indexOf(q) !== -1);
});
+ $("#thehistory .hist-day-group").each(function () {
+ var hasVisible = $(this).find('.hist-entry:visible').length > 0;
+ $(this).toggle(!q.length || hasVisible);
+ });
}
$(document).on('input.histfilter', '#histFilter', applyHistoryFilter);
+ $(document).on('click', '#thehistory .hist-day-header', function () {
+ $(this).closest('.hist-day-group').toggleClass('collapsed');
+ });
+
ftapi.events.on('newHistory', function (data) {
renderHistoryItem(data, $historyItem, "#thehistory", "histart");
applyHistoryFilter();
@@ -270,9 +303,10 @@ firetable.ui.setupRoomEvents = function () {
var doTheScrollThing = firetable.utilities.isChatPrettyMuchAtBottom();
if (showPlaycount) {
- $("#playCount").text(data.adamData.playcount + " plays");
+ var count = data.adamData.playcount;
+ $("#playCount").text(firetable.utilities.pluralize(count, "play"));
$(".npmsg" + data.cid).last().find(".npmsg").html(
- 'DJ
' + nicename + ' started playing
' + data.adamData.track_name + ' by
' + data.adamData.artist + 'This song has been played ' + data.adamData.playcount + ' times.'
+ 'DJ
' + nicename + ' started playing
' + data.adamData.track_name + ' by
' + data.adamData.artist + 'This song has been played ' + firetable.utilities.pluralize(count, "time") + '.'
);
} else {
$("#playCount").text("");
@@ -313,7 +347,7 @@ firetable.ui.setupRoomEvents = function () {
}
if (firetable.tagUpdate.adamData.playcount > 0) {
showPlaycount = true;
- $("#playCount").text(firetable.tagUpdate.adamData.playcount + " plays");
+ $("#playCount").text(firetable.utilities.pluralize(firetable.tagUpdate.adamData.playcount, "play"));
}
}
@@ -401,7 +435,7 @@ firetable.ui.setupRoomEvents = function () {
var doTheScrollThing = firetable.utilities.isChatPrettyMuchAtBottom();
var npmsgHTML;
if (showPlaycount) {
- npmsgHTML = '
DJ ' + nicename + ' started playing ' + data.title + ' by ' + data.artist + '
This song has been played ' + firetable.tagUpdate.adamData.playcount + ' times.
';
+ npmsgHTML = '
DJ ' + nicename + ' started playing ' + data.title + ' by ' + data.artist + '
This song has been played ' + firetable.utilities.pluralize(firetable.tagUpdate.adamData.playcount, "time") + '.
';
} else {
npmsgHTML = '
DJ ' + nicename + ' started playing ' + data.title + ' by ' + data.artist + '
';
}
@@ -471,7 +505,7 @@ firetable.ui.setupRoomEvents = function () {
if (data.hasOwnProperty(key)) {
cnt = countr;
var removeMe = data[key].removeAfter ? "departure_board" : "";
- html += '
' + countr + '. ' + data[key].name +
' ' + removeMe + ' ';
@@ -516,14 +550,14 @@ firetable.ui.setupRoomEvents = function () {
}
// Fill empty spots
if (countr < 4) {
- html += '
';
+ html += '
';
countr++;
for (var i = countr; i < 4; i++) {
html += '
';
}
}
} else {
- html += '
';
+ html += '
';
for (var i = 0; i < 3; i++) {
html += '
';
}
@@ -562,6 +596,13 @@ firetable.ui.setupRoomEvents = function () {
}
});
+ // Re-render deck when user data arrives (mod status affects button visibility)
+ ftapi.events.on("usersChanged", function () {
+ if (firetable.tableData !== undefined) {
+ ftapi.events.emit("tableChanged", firetable.tableData);
+ }
+ });
+
// ── Spotlight (Active DJ Index) ──
ftapi.events.on("spotlightStateChanged", function (data) {
firetable.playdex = data;
@@ -614,7 +655,7 @@ firetable.ui.setupRoomEvents = function () {
$('.customColorStyles').remove();
$("head").append(
"