diff --git a/manifest.json b/manifest.json index 1dc21ca..c655174 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Sam's Twitch Live", "description": "Sam's Twitch Live: Your Following Channels, Constantly in View.", - "version": "1.1", + "version": "1.2", "author": "yungsamd17", "homepage_url": "https://github.com/yungsamd17/Twitch-Live", "icons": diff --git a/popup.html b/popup.html index 55271cd..9272d60 100644 --- a/popup.html +++ b/popup.html @@ -11,36 +11,63 @@ - -
-
-
@@ -50,7 +77,6 @@

Settings

-
@@ -65,7 +91,7 @@

Settings

- Open streams in player + Open streams in player
-
+
Custom badge color
- +
-
- diff --git a/src/css/auth.css b/src/css/auth.css index 23e8e1a..2a54109 100644 --- a/src/css/auth.css +++ b/src/css/auth.css @@ -26,7 +26,7 @@ .auth-button { margin: 10px; - border-radius: 8px; + border-radius: 5px; font-size: 1rem; padding: 10px; background-color: #9146ff; diff --git a/src/css/main.css b/src/css/main.css index d6489ea..cc788f0 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -36,13 +36,13 @@ body { } .search-container { - width: 50%; + width: auto; display: inline-flex; height: 100%; } .search-container input[type="text"] { - width: 100%; + width: 150px; background-color: #242429; color: #fff; border: 2px solid #242429; @@ -87,14 +87,14 @@ body { color: #fff; font-weight: bold; background-color: #38383f; - padding: 7px 7px 5.5px 7px; + padding: 6.5px 7px; border: 0; border-radius: 5px; text-decoration: none; - margin: 10px; + margin: 10px 10px 10px 0; display: inline; align-content: center; - font-size: 0.9rem; + font-size: 1rem; cursor: pointer; user-select: none; } @@ -107,13 +107,6 @@ body { background-color: #772ce8; } -.navbar-icon-link { - padding: 6.5px 7.5px; - margin-left: 0; - margin-right: 10px; - font-size: 1rem; -} - .refresh-button { margin-left: 0px; border-radius: 0 5px 5px 0; @@ -123,6 +116,74 @@ body { +/* FILTER DROPDOWN */ +.dropdown { + display: none; + align-items: center; +} + +.dropdown-content { + position: absolute; + top: 100%; + right: 26%; + background-color: #242429; + padding: 0; + width: 176px; + height: auto; + border: 1px solid #38383f; + border-radius: 5px; + display: flex; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 0 10px 6px rgba(0, 0, 0, 0.3); +} + +.dropdown-body { + color: #fff; + width: 100%; + padding: 5px 0; +} + +.filter-option { + height: 30px; + font-size: 1rem; + padding: 0 5px; +} + +.filter-button { + display: inline-flex; + align-items: center; + height: 100%; + width: 100%; + padding: 0 6px; + background-color: #242429; + border: 0; + border-radius: 3px; + cursor: pointer; +} + +.filter-button:hover { + background-color: #38383f; +} + +.filter-button:active { + background-color: #54545f; +} + +.active { + background-color: #9146ff; +} + +.active:hover { + background-color: #772ce8; +} + +.active:active { + background-color: #5c16c5; +} + +.filter-icon { + margin-right: 8px; +} + /* MAIN ELEMENT CLASSES */ .streams { @@ -179,11 +240,11 @@ body { position: absolute; right: 10.1%; bottom: 15.5%; - background: rgba(0, 0, 0, 0.8); + background: rgba(0, 0, 0, 0.6); color: rgba(255, 255, 255, 0.8); font-size: 0.78rem; font-weight: 600; - padding: 2px 2px 1px 3px;; + padding: 2px 2px 1px 3px; border-radius: 4px 0; text-shadow: 0 0 4px #000; z-index: 900; @@ -231,6 +292,6 @@ body { color: #fff; text-align: center; font-weight: bold; - font-size: 1rem; - padding: 10px; + font-size: 1.1rem; + padding: 42px 0 0 0; } diff --git a/src/css/settings.css b/src/css/settings.css index 43fa4e3..276cb5b 100644 --- a/src/css/settings.css +++ b/src/css/settings.css @@ -229,11 +229,11 @@ input:checked+.slider:before { } .settings-link:hover { - background-color: #9146ff; + background-color: #54545f; } .settings-link:active { - background-color: #772ce8; + background-color: #9146ff; } /* LOGOUNT BUTTON */ diff --git a/src/css/tooltips.css b/src/css/tooltips.css index 179f5cf..78c418a 100644 --- a/src/css/tooltips.css +++ b/src/css/tooltips.css @@ -1,26 +1,26 @@ /* SETTINGS TOOLTIP */ -[aria-label] { +[settings-label] { position: relative; } -[aria-label]:after, -[aria-label]:before { +[settings-label]:after, +[settings-label]:before { content: ""; position: absolute; z-index: 500; pointer-events: none; } -[aria-label]:after { - content: attr(aria-label); +[settings-label]:after { + content: attr(settings-label); display: block; position: absolute; top: 133%; right: 0%; z-index: 500; pointer-events: none; - padding: 6px 5px; + padding: 5px; line-height: 15px; white-space: nowrap; text-decoration: none; @@ -33,7 +33,7 @@ opacity: 0; } -[aria-label]:before { +[settings-label]:before { content: ""; border-style: solid; border-width: 0 8px 10px 8px; @@ -44,45 +44,45 @@ opacity: 0; } -[aria-label]:hover:after { +[settings-label]:hover:after { opacity: 1; transition-delay: 0.3s; } -[aria-label]:hover:before { +[settings-label]:hover:before { opacity: 1; transition-delay: 0.3s; } -[aria-label]:not(:hover):after, -[aria-label]:not(:hover):before { +[settings-label]:not(:hover):after, +[settings-label]:not(:hover):before { transition-delay: 0s; } /* TWITCH LIVE FOLLOWING TOOLTIP */ -[aria-label-2] { +[twitch-label] { position: relative; } -[aria-label-2]:after, -[aria-label-2]:before { +[twitch-label]:after, +[twitch-label]:before { content: ""; position: absolute; z-index: 500; pointer-events: none; } -[aria-label-2]:after { - content: attr(aria-label-2); +[twitch-label]:after { + content: attr(twitch-label); display: block; position: absolute; top: 133%; right: -132%; z-index: 500; pointer-events: none; - padding: 6px 5px; + padding: 5px; line-height: 15px; white-space: nowrap; text-decoration: none; @@ -95,7 +95,7 @@ opacity: 0; } -[aria-label-2]:before { +[twitch-label]:before { content: ""; border-style: solid; border-width: 0 8px 10px 8px; @@ -106,45 +106,45 @@ opacity: 0; } -[aria-label-2]:hover:after { +[twitch-label]:hover:after { opacity: 1; transition-delay: 0.3s; } -[aria-label-2]:hover:before { +[twitch-label]:hover:before { opacity: 1; transition-delay: 0.3s; } -[aria-label-2]:not(:hover):after, -[aria-label-2]:not(:hover):before { +[twitch-label]:not(:hover):after, +[twitch-label]:not(:hover):before { transition-delay: 0s; } /* REFRESH BUTTON TOOLTIP */ -[aria-label-3] { +[refresh-label] { position: relative; } -[aria-label-3]:after, -[aria-label-3]:before { +[refresh-label]:after, +[refresh-label]:before { content: ""; position: absolute; z-index: 500; pointer-events: none; } -[aria-label-3]:after { - content: attr(aria-label-3); +[refresh-label]:after { + content: attr(refresh-label); display: block; position: absolute; top: 133%; left: -125%; z-index: 500; pointer-events: none; - padding: 6px 5px; + padding: 5px; line-height: 15px; white-space: nowrap; text-decoration: none; @@ -157,7 +157,7 @@ opacity: 0; } -[aria-label-3]:before { +[refresh-label]:before { content: ""; border-style: solid; border-width: 0 8px 10px 8px; @@ -168,45 +168,45 @@ opacity: 0; } -[aria-label-3]:hover:after { +[refresh-label]:hover:after { opacity: 1; transition-delay: 0.3s; } -[aria-label-3]:hover:before { +[refresh-label]:hover:before { opacity: 1; transition-delay: 0.3s; } -[aria-label-3]:not(:hover):after, -[aria-label-3]:not(:hover):before { +[refresh-label]:not(:hover):after, +[refresh-label]:not(:hover):before { transition-delay: 0s; } /* "Open streams in Player" SETTINGS OPTION INFO LINK TOOLTIP */ -[aria-label-4] { +[info-label] { position: relative; } -[aria-label-4]:after, -[aria-label-4]:before { +[info-label]:after, +[info-label]:before { content: ""; position: absolute; z-index: 500; pointer-events: none; } -[aria-label-4]:after { - content: attr(aria-label-4); +[info-label]:after { + content: attr(info-label); display: block; position: absolute; top: 174%; - left: -420%; + left: -450%; z-index: 500; pointer-events: none; - padding: 6px 5px; + padding: 5px; line-height: 15px; white-space: nowrap; text-decoration: none; @@ -219,7 +219,7 @@ opacity: 0; } -[aria-label-4]:before { +[info-label]:before { content: ""; border-style: solid; border-width: 0 8px 10px 8px; @@ -230,17 +230,79 @@ opacity: 0; } -[aria-label-4]:hover:after { +[info-label]:hover:after { opacity: 1; transition-delay: 0.3s; } -[aria-label-4]:hover:before { +[info-label]:hover:before { opacity: 1; transition-delay: 0.3s; } -[aria-label-4]:not(:hover):after, -[aria-label-4]:not(:hover):before { +[info-label]:not(:hover):after, +[info-label]:not(:hover):before { transition-delay: 0s; } + + +/* FILTER BUTTON TOOLTIP */ + +[filter-label] { + position: relative; +} + +[filter-label]:after, +[filter-label]:before { + content: ""; + position: absolute; + z-index: 500; + pointer-events: none; +} + +[filter-label]:after { + content: attr(filter-label); + display: block; + position: absolute; + top: 133%; + left: -99.5%; + z-index: 500; + pointer-events: none; + padding: 5px; + line-height: 15px; + white-space: nowrap; + text-decoration: none; + text-indent: 0; + overflow: visible; + font-size: .8em; + color: #000; + background-color: #fff; + border-radius: 5px; + opacity: 0; +} + +[filter-label]:before { + content: ""; + border-style: solid; + border-width: 0 8px 10px 8px; + border-color: transparent transparent #fff transparent; + top: 35px; + right: 50%; + transform: translateX(50%); + opacity: 0; +} + +[filter-label]:hover:after { + opacity: 1; + transition-delay: 0.3s; +} + +[filter-label]:hover:before { + opacity: 1; + transition-delay: 0.3s; +} + +[filter-label]:not(:hover):after, +[filter-label]:not(:hover):before { + transition-delay: 0s; +} \ No newline at end of file diff --git a/src/js/background.js b/src/js/background.js index 92039c6..a66732c 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -188,6 +188,7 @@ const getLiveTwitchStreams = async () => { channelName: stream["user_name"], viewerCount: stream["viewer_count"], liveTime: getTimePassed(stream["started_at"]), + startedAt: getStartedAtTime(stream["started_at"]), })), }); diff --git a/src/js/main.js b/src/js/main.js index 6c860a1..83e6c74 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -84,8 +84,56 @@ const loadTwitchContent = async () => { } if (res.twitchStreams) { + let filteredStreams = [...res.twitchStreams]; // Copy the array to avoid modifying the original + + // Filter/Sort options + const selectedFilter = getSelectedFilterOption(); + switch (selectedFilter) { + case "Broadcaster": + filteredStreams.sort((a, b) => (a.channelName > b.channelName ? 1 : -1)); + break; + case "Category": + filteredStreams.sort((a, b) => (a.gameName > b.gameName ? 1 : -1)); + break; + case "Uptime": + filteredStreams.sort((a, b) => { + const aUptime = new Date(a.startedAt).getTime(); + const bUptime = new Date(b.startedAt).getTime(); + return aUptime - bUptime; + }); + break; + case "Viewers (High to Low)": + filteredStreams.sort((a, b) => b.viewerCount - a.viewerCount); + break; + case "Viewers (Low to High)": + filteredStreams.sort((a, b) => a.viewerCount - b.viewerCount); + break; + case "Recently Started": + filteredStreams.sort((a, b) => { + const aStarted = new Date(a.startedAt).getTime(); + const bStarted = new Date(b.startedAt).getTime(); + return bStarted - aStarted; + }); + break; + case "Longest Running": + filteredStreams.sort((a, b) => { + const aStarted = new Date(a.startedAt).getTime(); + const bStarted = new Date(b.startedAt).getTime(); + const aRunning = a.runningAt ? new Date(a.runningAt).getTime() : Date.now(); + const bRunning = b.runningAt ? new Date(b.runningAt).getTime() : Date.now(); + const aDuration = aRunning - aStarted; + const bDuration = bRunning - bStarted; + + // console.log("aDuration:", aDuration, "bDuration:", bDuration); // debugging + return bDuration - aDuration; + }); + break; + default: + break; + } + const query = filterInput.value.toLowerCase(); - const filteredStreams = res.twitchStreams.filter( + filteredStreams = filteredStreams.filter( (stream) => stream.channelName.toLowerCase().includes(query) || stream.title.toLowerCase().includes(query) || @@ -105,6 +153,7 @@ const loadTwitchContent = async () => { const uptime = document.createElement("div"); uptime.setAttribute("class", "stream-uptime"); uptime.innerHTML = `${stream.liveTime}`; + uptime.setAttribute("title", `${stream.startedAt}`); streamThumbnail.appendChild(uptime); const thumbnail = document.createElement("img"); @@ -125,6 +174,7 @@ const loadTwitchContent = async () => { const categoryAndViewCount = document.createElement("span"); categoryAndViewCount.setAttribute("class", "stream-game-and-viewers"); categoryAndViewCount.innerHTML = `${stream.gameName} - ${formatViewerCount(stream.viewerCount)} viewers`; + categoryAndViewCount.setAttribute("title", `${stream.gameName} - ${formatViewerCount(stream.viewerCount)} viewers`); streamDetails.appendChild(categoryAndViewCount); const title = document.createElement("span"); @@ -159,6 +209,55 @@ const handleRefreshButtonClick = () => { loadTwitchContent(); }; +// Function to get the selected filter option +const getSelectedFilterOption = () => { + const broadcasterButton = document.getElementById("broadcasterButton"); + const categoryButton = document.getElementById("categoryButton"); + const viewersHighToLowButton = document.getElementById("viewersHighToLowButton"); + const viewersLowToHighButton = document.getElementById("viewersLowToHighButton"); + const startedButton = document.getElementById("startedButton"); + const runningButton = document.getElementById("runningButton"); + + if (broadcasterButton && broadcasterButton.classList.contains("active")) { + return "Broadcaster"; + } else if (categoryButton && categoryButton.classList.contains("active")) { + return "Category"; + } else if (viewersHighToLowButton && viewersHighToLowButton.classList.contains("active")) { + return "Viewers (High to Low)"; + } else if (viewersLowToHighButton && viewersLowToHighButton.classList.contains("active")) { + return "Viewers (Low to High)"; + } else if (startedButton && startedButton.classList.contains("active")) { + return "Recently Started"; + } else if (runningButton && runningButton.classList.contains("active")) { + return "Longest Running"; + } + + return "viewersHighToLowButton"; // Default filter option +}; + +// Function to set the selected filter option +const setSelectedFilterOption = (buttonId) => { + // Remove the 'active' class from all filter buttons + const filterButtons = document.querySelectorAll('.filter-button'); + filterButtons.forEach(button => button.classList.remove('active')); + + // Add the 'active' class to the clicked filter button + const selectedButton = document.getElementById(buttonId); + if (selectedButton) { + selectedButton.classList.add('active'); + } +}; + +// Event listeners for the filter buttons +const filterButtons = document.querySelectorAll('.filter-button'); +filterButtons.forEach(button => { + button.addEventListener('click', function() { + const buttonId = this.id; + setSelectedFilterOption(buttonId); + loadTwitchContent(); // Reload content based on the selected filter + }); +}); + // Event listeners for the search input and refresh button filterInput.addEventListener("input", loadTwitchContent); refreshButton.addEventListener("click", handleRefreshButtonClick); diff --git a/src/js/settings.js b/src/js/settings.js index 3daecd5..565d2c4 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -41,38 +41,35 @@ const colorInput = document.getElementById("colorInput"); console.log("Toggle Switch Status - Open in New Window:", openInNewWindowToggle.checked);*/ // Custom color badge input -colorInput.addEventListener("keydown", function (event) { - if (event.key === "Enter") { - const color = this.value.trim(); +colorInput.addEventListener("focus", function () { + this.addEventListener("input", handleColorInput); +}); + +colorInput.addEventListener("blur", function () { + this.removeEventListener("input", handleColorInput); +}); + +function handleColorInput() { + const color = this.value.trim(); + + // Check if the entered color is a valid HEX format and starts with "#" + if (/^#[0-9A-Fa-f]{6}$/.test(color) && color.startsWith("#") && color.length >= 7) { + // Clear any existing timeout + clearTimeout(this.timeoutId); - // Check if the entered color is a valid HEX format and starts with "#" - if (/^#[0-9A-Fa-f]{6}$/.test(color) && color.startsWith("#")) { + // Schedule the update after a delay (e.g., 100 milliseconds) + this.timeoutId = setTimeout(() => { chrome.storage.local.set({ customBadgeColor: color }, () => { // Update the badge color chrome.action.setBadgeBackgroundColor({ color }); }); - } else { - // If the input is invalid or empty, set to default color and clear the input - chrome.storage.local.set({ customBadgeColor: "" }, () => { - this.value = ""; - chrome.action.setBadgeBackgroundColor({ color: "#666666" }); - }); - } - - // Prevent the default form submission behavior - event.preventDefault(); + }, 100); + } else if (color.length === 0) { + // If the input is empty, set to default color + chrome.storage.local.set({ customBadgeColor: "" }, () => { + chrome.action.setBadgeBackgroundColor({ color: "#666666" }); + }); } -}); - -// Settings Modal -var modal = document.getElementById("settingsModal"); -var btn = document.getElementById("settingsBtn"); -var span = document.getElementsByClassName("settings-close-btn")[0]; -btn.onclick = function() { - modal.style.display = "flex"; -} -span.onclick = function() { - modal.style.display = "none"; } // Function to handle the logout button click @@ -95,3 +92,28 @@ const logoutButton = document.getElementById("logoutBtn"); if (logoutButton) { logoutButton.addEventListener("click", handleLogoutButtonClick); } + +// Filter dropdown +var dropdown = document.getElementById("filterDropdown"); +var filterBtn = document.getElementById("filterButton"); +filterBtn.onclick = function(event) { + dropdown.style.display = dropdown.style.display === "flex" ? "none" : "flex"; + event.stopPropagation(); +}; + +window.onclick = function(event) { + if (!event.target.matches('.dropdown-content') && dropdown.style.display === "flex") { + dropdown.style.display = "none"; + } +} + +// Settings Modal +var modal = document.getElementById("settingsModal"); +var btn = document.getElementById("settingsBtn"); +var span = document.getElementsByClassName("settings-close-btn")[0]; +btn.onclick = function() { + modal.style.display = "flex"; +} +span.onclick = function() { + modal.style.display = "none"; +} diff --git a/src/js/util.js b/src/js/util.js index 1b455c3..1442d44 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -1,3 +1,21 @@ +const getStartedAtTime = (startTime) => { + const options = { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: true, + }; + + const formattedTime = new Intl.DateTimeFormat('en-US', options) + .format(new Date(startTime)) + .replace(',', ''); + + return formattedTime; +}; + const getTimePassed = (startTime) => { let elapsedTime = Date.now() - Date.parse(startTime); const hours = Math.floor(elapsedTime / 3600000);