aboutsummaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-12-15 09:50:35 -0500
committerBobby <[email protected]>2024-12-15 09:50:35 -0500
commit06f23e7745c5875039720468db0a2786d2aaf848 (patch)
treecc49ba62e71c35b3294edc7b753b9627b2d2ac9c /static
parent2efc3e9fbb38e447c5e336dfea679644ea16af12 (diff)
downloadthatcomputerscientist-06f23e7745c5875039720468db0a2786d2aaf848.tar.xz
thatcomputerscientist-06f23e7745c5875039720468db0a2786d2aaf848.zip
moved announcements to administration. custom admin center. removed userpages app
Diffstat (limited to 'static')
-rw-r--r--static/css/core/home.css43
-rw-r--r--static/css/shared/core.css10
-rw-r--r--static/css/styles.css20
-rw-r--r--static/images/core/backgrounds/announcements_border.jpgbin0 -> 44000 bytes
-rw-r--r--static/images/core/gifs/hand.gif (renamed from static/images/gifs/hand.gif)bin1630 -> 1630 bytes
-rw-r--r--static/images/core/gifs/new_announcement.gif (renamed from static/images/gifs/new_announcement.gif)bin322 -> 322 bytes
-rw-r--r--static/images/core/gifs/update.gifbin0 -> 6109 bytes
-rw-r--r--static/js/libs/marquee.js357
-rw-r--r--static/js/libs/pamphlet.js53
-rw-r--r--static/js/shared/kawaiiBeatsPlayer.js938
-rw-r--r--static/js/shared/resolutionScaling.js136
11 files changed, 1036 insertions, 521 deletions
diff --git a/static/css/core/home.css b/static/css/core/home.css
index 79df5690..1b518b6b 100644
--- a/static/css/core/home.css
+++ b/static/css/core/home.css
@@ -56,6 +56,49 @@
width: 780px;
}
+#announcements {
+ background: url('../../images/core/backgrounds/announcements.png') no-repeat;
+ background-size: 780px 320px;
+ position: relative;
+ margin-top: 12px;
+ width: 780px;
+ height: 320px;
+}
+
+#announcement-container {
+ width: 580px;
+ height: 280px;
+ position: relative;
+ top: 8px;
+ left: 128px;
+ border: 16px solid transparent;
+ padding: 8px;
+ border-image: url('../../images/core/backgrounds/announcements_border.jpg') 45 round;
+}
+
+.announcement {
+ display: flex;
+ align-items: flex-start;
+ margin: 10px 0;
+ white-space: normal;
+ min-height: 30px;
+}
+
+.announcement-icon {
+ flex-shrink: 0;
+ margin-right: 8px;
+ margin-top: 4px;
+}
+
+.announcement-icon img {
+ height: 14px;
+ width: 30px;
+}
+
+.announcement-content {
+ flex-grow: 1;
+}
+
/*
#announcements {
background: url('../images/backgrounds/announcements.png') no-repeat;
diff --git a/static/css/shared/core.css b/static/css/shared/core.css
index fc369c39..e764da32 100644
--- a/static/css/shared/core.css
+++ b/static/css/shared/core.css
@@ -53,14 +53,14 @@ hr {
a,
a:link,
a:visited {
- color: #8DC6FF;
+ color: #8d8dff;
text-decoration: none;
}
a:hover,
a:active {
- color: #2381DF;
+ color: #df23c4;
}
a:hover {
@@ -208,12 +208,12 @@ img {
.navigation-title {
font-family: 'SweetFairy', sans-serif;
- filter: drop-shadow(2px 0 0 white) drop-shadow(0 2px 0 white) drop-shadow(-2px 0 0 white) drop-shadow(0 -2px 0 white) drop-shadow(0px 1px 1px #2381DF) drop-shadow(0px 1px 1px #2381DF);
- color: #2381DF;
+ filter: drop-shadow(2px 0 0 white) drop-shadow(0 2px 0 white) drop-shadow(-2px 0 0 white) drop-shadow(0 -2px 0 white) drop-shadow(0px 1px 1px #623795) drop-shadow(0px 1px 1px #623795);
+ color: #623795;
}
.navigation-title-container {
- background-color: #002B62;
+ background-color: rgba(173, 128, 236, 0.35);
padding: 10px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
diff --git a/static/css/styles.css b/static/css/styles.css
index 775ab13f..7300973d 100644
--- a/static/css/styles.css
+++ b/static/css/styles.css
@@ -18,7 +18,7 @@ body {
visibility: hidden !important;
}
-.skiptranslate > iframe {
+.skiptranslate>iframe {
display: none !important;
}
@@ -296,8 +296,6 @@ blockquote {
border-radius: 8px;
text-align: left !important;
overflow-x: auto;
-
- // Custom Horizontal Scrollbar
scrollbar-width: thin;
scrollbar-color: #311b4f #311b4f26;
}
@@ -507,24 +505,24 @@ blockquote {
margin-right: 0px;
}
-#article > h1 {
+#article>h1 {
font-size: 32px;
margin-bottom: 10px;
}
-#anonymous-profile-info > div,
-#anonymous-profile-info > #creds > div {
+#anonymous-profile-info>div,
+#anonymous-profile-info>#creds>div {
margin: 10px 0;
}
-#anonymous-profile-info > div > label,
-#anonymous-profile-info > #creds > div > label {
+#anonymous-profile-info>div>label,
+#anonymous-profile-info>#creds>div>label {
width: 200px;
display: inline-block;
}
-#anonymous-profile-info > div > input,
-#anonymous-profile-info > #creds > div > input {
+#anonymous-profile-info>div>input,
+#anonymous-profile-info>#creds>div>input {
width: 300px;
display: inline-block;
}
@@ -548,4 +546,4 @@ blockquote {
.subhead::first-letter {
initial-letter: 2;
margin: 0 10px 0 0;
-}
+} \ No newline at end of file
diff --git a/static/images/core/backgrounds/announcements_border.jpg b/static/images/core/backgrounds/announcements_border.jpg
new file mode 100644
index 00000000..0fee97f4
--- /dev/null
+++ b/static/images/core/backgrounds/announcements_border.jpg
Binary files differ
diff --git a/static/images/gifs/hand.gif b/static/images/core/gifs/hand.gif
index 70d574cd..70d574cd 100644
--- a/static/images/gifs/hand.gif
+++ b/static/images/core/gifs/hand.gif
Binary files differ
diff --git a/static/images/gifs/new_announcement.gif b/static/images/core/gifs/new_announcement.gif
index ffef772d..ffef772d 100644
--- a/static/images/gifs/new_announcement.gif
+++ b/static/images/core/gifs/new_announcement.gif
Binary files differ
diff --git a/static/images/core/gifs/update.gif b/static/images/core/gifs/update.gif
new file mode 100644
index 00000000..17bf905b
--- /dev/null
+++ b/static/images/core/gifs/update.gif
Binary files differ
diff --git a/static/js/libs/marquee.js b/static/js/libs/marquee.js
new file mode 100644
index 00000000..2d251170
--- /dev/null
+++ b/static/js/libs/marquee.js
@@ -0,0 +1,357 @@
+/**
+ * A class that implements marquee functionality for HTML elements
+ * Original source: https://shi.foo/static/js/libs/marquee.js
+ * Credit must be given if this code is used in any way
+ */
+class Marquee {
+ /** @readonly */
+ static DIRECTION = {
+ LEFT: 'left',
+ RIGHT: 'right',
+ UP: 'up',
+ DOWN: 'down'
+ };
+
+ /** @readonly */
+ static BEHAVIOR = {
+ SCROLL: 'scroll',
+ SLIDE: 'slide',
+ ALTERNATE: 'alternate'
+ };
+
+ /** @readonly */
+ static DEFAULTS = {
+ behavior: 'scroll',
+ bgcolor: null,
+ direction: 'left',
+ height: null,
+ width: null,
+ hspace: 0,
+ vspace: 0,
+ loop: -1,
+ scrollamount: 6,
+ scrolldelay: 85,
+ truespeed: false
+ };
+
+ /**
+ * Creates a new Marquee instance
+ * @param {HTMLElement} element - The element to transform into a marquee
+ * @param {Object} [options] - Configuration options
+ * @param {string} [options.behavior] - Scroll behavior (scroll, slide, alternate)
+ * @param {string} [options.bgcolor] - Background color
+ * @param {string} [options.direction] - Scroll direction (left, right, up, down)
+ * @param {string|number} [options.height] - Container height
+ * @param {string|number} [options.width] - Container width
+ * @param {number} [options.hspace] - Horizontal margin
+ * @param {number} [options.vspace] - Vertical margin
+ * @param {number} [options.loop] - Number of loops (-1 for infinite)
+ * @param {number} [options.scrollamount] - Pixels to move per step
+ * @param {number} [options.scrolldelay] - Delay between steps in ms
+ * @param {boolean} [options.truespeed] - Whether to respect exact scroll delay
+ * @throws {Error} If invalid element is provided
+ */
+ constructor(element, options = {}) {
+ if (!(element instanceof HTMLElement)) {
+ throw new Error('Invalid element provided to Marquee');
+ }
+
+ this.element = element;
+
+ const attributeOptions = {
+ behavior: element.getAttribute('behavior'),
+ bgcolor: element.getAttribute('bgcolor'),
+ direction: element.getAttribute('direction'),
+ height: element.getAttribute('height'),
+ width: element.getAttribute('width'),
+ hspace: element.getAttribute('hspace'),
+ vspace: element.getAttribute('vspace'),
+ loop: element.getAttribute('loop'),
+ scrollamount: element.getAttribute('scrollamount'),
+ scrolldelay: element.getAttribute('scrolldelay'),
+ truespeed: element.hasAttribute('truespeed')
+ };
+
+ this.config = {
+ ...Marquee.DEFAULTS,
+ ...Object.fromEntries(Object.entries(attributeOptions).filter(([_, v]) => v != null)),
+ ...this._normalizeOptions(options)
+ };
+
+ this._rafId = null;
+ this._currentPos = 0;
+ this._lastTime = Date.now();
+ this._isPaused = false;
+ this._loopCount = 0;
+ this._originalContent = '';
+
+ this._setupContainer();
+ this._setupContent();
+ this._setupDimensions();
+ this._setupEventListeners();
+
+ this._bindInlineHandlers();
+ this.start();
+ }
+
+ /**
+ * Normalizes numeric options by parsing them to integers
+ * @private
+ * @param {Object} options - Options to normalize
+ * @returns {Object} Normalized options
+ */
+ _normalizeOptions(options) {
+ return {
+ ...options,
+ scrollamount: parseInt(options.scrollamount) || Marquee.DEFAULTS.scrollamount,
+ scrolldelay: parseInt(options.scrolldelay) || Marquee.DEFAULTS.scrolldelay,
+ loop: parseInt(options.loop) || Marquee.DEFAULTS.loop,
+ hspace: parseInt(options.hspace) || Marquee.DEFAULTS.hspace,
+ vspace: parseInt(options.vspace) || Marquee.DEFAULTS.vspace
+ };
+ }
+
+ /**
+ * Sets up the container element
+ * @private
+ */
+ _setupContainer() {
+ this.container = document.createElement('div');
+ this.container.style.overflow = 'hidden';
+ this.container.style.position = 'relative';
+ this.container.style.width = this.config.width || '100%';
+ this.container.style.height = this.config.height || 'auto';
+
+ if (this.config.bgcolor) {
+ this.container.style.backgroundColor = this.config.bgcolor;
+ }
+
+ this.container.style.margin = `${this.config.vspace}px ${this.config.hspace}px`;
+ this._originalContent = this.element.innerHTML;
+ this.element.innerHTML = '';
+ this.element.appendChild(this.container);
+
+ const eventHandlers = this.element.attributes;
+ for (let i = 0; i < eventHandlers.length; i++) {
+ const attr = eventHandlers[i];
+ if (attr.name.startsWith('on')) {
+ this.container[attr.name] = this.element[attr.name];
+ }
+ }
+ }
+
+ /**
+ * Sets up the content wrapper
+ * @private
+ */
+ _setupContent() {
+ this.contentWrapper = document.createElement('div');
+ this.contentWrapper.style.position = 'absolute';
+ this.contentWrapper.style.width = '100%';
+ this.contentWrapper.innerHTML = this._originalContent.trim();
+
+ if (this.config.direction === 'up' || this.config.direction === 'down') {
+ this.contentWrapper.style.display = 'block';
+ } else {
+ this.contentWrapper.style.whiteSpace = 'nowrap';
+ }
+
+ if (this.config.behavior !== Marquee.BEHAVIOR.ALTERNATE) {
+ const children = Array.from(this.contentWrapper.children);
+
+ children.forEach(child => {
+ const clone = child.cloneNode(true);
+ this.contentWrapper.appendChild(clone);
+ });
+ }
+
+ this.container.appendChild(this.contentWrapper);
+ }
+
+ /**
+ * Sets up initial dimensions and positions
+ * @private
+ */
+ _setupDimensions() {
+ requestAnimationFrame(() => {
+ switch (this.config.direction) {
+ case Marquee.DIRECTION.LEFT:
+ this._currentPos = this.container.offsetWidth;
+ break;
+ case Marquee.DIRECTION.RIGHT:
+ this._currentPos = -this.contentWrapper.offsetWidth;
+ break;
+ case Marquee.DIRECTION.UP:
+ this._currentPos = this.container.offsetHeight;
+ break;
+ case Marquee.DIRECTION.DOWN:
+ this._currentPos = -this.contentWrapper.offsetHeight;
+ break;
+ }
+ this._applyPosition();
+ });
+ }
+
+ /**
+ * Sets up event listeners for hover and visibility
+ * @private
+ */
+ _setupEventListeners() {
+ if (!this.config.truespeed) {
+ this.container.addEventListener('mouseenter', () => this.pause());
+ this.container.addEventListener('mouseleave', () => this.start());
+ }
+
+ this._visibilityHandler = () => {
+ if (document.hidden) {
+ this.pause();
+ } else if (!this._isPaused) {
+ this.start();
+ }
+ };
+ document.addEventListener('visibilitychange', this._visibilityHandler);
+ }
+
+ /**
+ * Sets up inline event handlers
+ * @private
+ */
+ _bindInlineHandlers() {
+ if (this.element.hasAttribute('onmouseover')) {
+ const originalStop = this.element.getAttribute('onmouseover');
+ this.element.removeAttribute('onmouseover');
+ this.element.addEventListener('mouseover', () => this.pause());
+ }
+
+ if (this.element.hasAttribute('onmouseout')) {
+ const originalStart = this.element.getAttribute('onmouseout');
+ this.element.removeAttribute('onmouseout');
+ this.element.addEventListener('mouseout', () => this.start());
+ }
+ }
+
+ /**
+ * Applies the current position to the content wrapper
+ * @private
+ */
+ _applyPosition() {
+ const isVertical = this.config.direction === 'up' || this.config.direction === 'down';
+ const transform = isVertical ?
+ `translateY(${this._currentPos}px)` :
+ `translateX(${this._currentPos}px)`;
+ this.contentWrapper.style.transform = transform;
+ }
+
+ /**
+ * Animation frame handler
+ * @private
+ */
+ _animate() {
+ const currentTime = Date.now();
+ const deltaTime = currentTime - this._lastTime;
+
+ if (deltaTime >= this.config.scrolldelay) {
+ this._lastTime = currentTime;
+ const pixelsToMove = this.config.scrollamount;
+
+ switch (this.config.direction) {
+ case Marquee.DIRECTION.LEFT:
+ this._currentPos -= pixelsToMove;
+ if (this._currentPos <= -this.contentWrapper.offsetWidth / 2) {
+ this._currentPos = this.container.offsetWidth;
+ this._handleLoop();
+ }
+ break;
+
+ case Marquee.DIRECTION.RIGHT:
+ this._currentPos += pixelsToMove;
+ if (this._currentPos >= this.container.offsetWidth) {
+ this._currentPos = -this.contentWrapper.offsetWidth / 2;
+ this._handleLoop();
+ }
+ break;
+
+ case Marquee.DIRECTION.UP:
+ this._currentPos -= pixelsToMove;
+ if (this._currentPos <= -this.contentWrapper.offsetHeight / 2) {
+ this._currentPos = this.container.offsetHeight;
+ this._handleLoop();
+ }
+ break;
+
+ case Marquee.DIRECTION.DOWN:
+ this._currentPos += pixelsToMove;
+ if (this._currentPos >= this.container.offsetHeight) {
+ this._currentPos = -this.contentWrapper.offsetHeight / 2;
+ this._handleLoop();
+ }
+ break;
+ }
+
+ this._applyPosition();
+ }
+
+ if (!this._isPaused) {
+ this._rafId = requestAnimationFrame(() => this._animate());
+ }
+ }
+
+ /**
+ * Handles loop counting and stopping
+ * @private
+ */
+ _handleLoop() {
+ if (this.config.loop !== -1) {
+ this._loopCount++;
+ if (this._loopCount >= this.config.loop) {
+ this.stop();
+ }
+ }
+ }
+
+ /**
+ * Starts or resumes the marquee animation
+ * @public
+ */
+ start() {
+ this._isPaused = false;
+ this._lastTime = Date.now();
+ if (!this._rafId) {
+ this._animate();
+ }
+ }
+
+ /**
+ * Pauses the marquee animation
+ * @public
+ */
+ pause() {
+ this._isPaused = true;
+ if (this._rafId) {
+ cancelAnimationFrame(this._rafId);
+ this._rafId = null;
+ }
+ }
+
+ /**
+ * Stops the marquee animation and resets position
+ * @public
+ */
+ stop() {
+ this.pause();
+ this._currentPos = 0;
+ this._loopCount = 0;
+ this._applyPosition();
+ }
+
+ /**
+ * Cleans up the marquee and restores original content
+ * @public
+ */
+ destroy() {
+ this.stop();
+ this.container.remove();
+ this.element.innerHTML = this._originalContent;
+ document.removeEventListener('visibilitychange', this._visibilityHandler);
+ }
+} \ No newline at end of file
diff --git a/static/js/libs/pamphlet.js b/static/js/libs/pamphlet.js
index 880f6c2e..0d9478e1 100644
--- a/static/js/libs/pamphlet.js
+++ b/static/js/libs/pamphlet.js
@@ -1,7 +1,23 @@
+/**
+ * A self-contained library for managing pamphlets
+ * Original source: https://shi.foo/static/js/libs/pamphlet.js
+ * Credit must be given if this code is used in any way
+ * @version 1.0.0
+ * @module Pamphlet
+ */
(function (window) {
'use strict';
+ /**
+ * Manages pamphlets
+ */
class Pamphlet {
+ /**
+ * Creates a new Pamphlet instance
+ * @param {Object} config - Configuration options
+ * @param {string} [config.server='/services/pamphlet'] - Server endpoint for pamphlet content
+ * @param {number} [config.refreshInterval=3600000] - Refresh interval in milliseconds (0 to disable)
+ */
constructor(config = {}) {
this.config = {
server: config.server || '/services/pamphlet',
@@ -9,6 +25,7 @@
...config
};
+ /** @type {Map<string, {element: HTMLElement, style: string}>} */
this.slots = new Map();
if (document.readyState === 'loading') {
@@ -18,6 +35,10 @@
}
}
+ /**
+ * Initializes the Pamphlet instance by scanning for slots and setting up refresh interval
+ * @private
+ */
init() {
this.scanForSlots();
@@ -26,6 +47,10 @@
}
}
+ /**
+ * Scans the document for pamphlet elements and sets them up
+ * @private
+ */
scanForSlots() {
const elements = document.querySelectorAll('.pamphlet');
elements.forEach(element => {
@@ -36,6 +61,12 @@
});
}
+ /**
+ * Determines the style of a pamphlet element based on its classes
+ * @private
+ * @param {HTMLElement} element - Element to check
+ * @returns {string|null} Style name or null if no valid style found
+ */
getStyle(element) {
const classes = element.classList;
if (classes.contains('pamphlet-banner')) return 'banner';
@@ -44,6 +75,12 @@
return null;
}
+ /**
+ * Sets up a new pamphlet slot
+ * @private
+ * @param {HTMLElement} element - Element to set up as a slot
+ * @param {string} style - Style of the pamphlet
+ */
setupSlot(element, style) {
const slotId = `slot-${Math.random().toString(36).substring(2, 9)}`;
element.setAttribute('data-pamphlet-id', slotId);
@@ -56,6 +93,11 @@
this.loadPamphlet(slotId);
}
+ /**
+ * Loads pamphlet content for a specific slot
+ * @private
+ * @param {string} slotId - ID of the slot to load content for
+ */
loadPamphlet(slotId) {
const slot = this.slots.get(slotId);
if (!slot) return;
@@ -76,12 +118,21 @@
}
}
+ /**
+ * Refreshes all pamphlet slots
+ * @public
+ */
refreshAll() {
this.slots.forEach((slot, slotId) => {
this.loadPamphlet(slotId);
});
}
+ /**
+ * Refreshes a specific pamphlet element
+ * @public
+ * @param {HTMLElement} element - Element to refresh
+ */
refresh(element) {
const slotId = element.getAttribute('data-pamphlet-id');
if (slotId) {
@@ -92,4 +143,4 @@
window.Pamphlet = Pamphlet;
-})(window);
+})(window); \ No newline at end of file
diff --git a/static/js/shared/kawaiiBeatsPlayer.js b/static/js/shared/kawaiiBeatsPlayer.js
index c43d770a..9893f734 100644
--- a/static/js/shared/kawaiiBeatsPlayer.js
+++ b/static/js/shared/kawaiiBeatsPlayer.js
@@ -1,22 +1,51 @@
-// Collection of random anime artwork
+/**
+ * Original source: https://shi.foo/static/js/shared/kawaiiBeatsPlayer.js
+ * Credit must be given if this code is used in any way
+ * @fileoverview Audio player implementation with artwork management and persistent state
+ * @version 1.0.0
+ */
+/** @type {string[]} Artwork collection for random assignment */
const artworkCollection = [
- 'https://i.pinimg.com/enabled/564x/e2/5d/31/e25d3199f73c9453035727f8c7a70170.jpg',
- 'https://i.pinimg.com/enabled/564x/5f/ed/28/5fed282cff8d22ac857e2a489031d05a.jpg',
- 'https://i.pinimg.com/736x/05/a8/71/05a87162a78e2cad2ffe0a9eac6b4e2c.jpg',
- 'https://i.pinimg.com/736x/c6/ac/13/c6ac139ed02c9accd34dbb16d7466025.jpg',
- 'https://i.pinimg.com/736x/72/69/c3/7269c3d939764b024da9a6869dc59a0f.jpg',
- 'https://i.pinimg.com/enabled/564x/cc/5c/6f/cc5c6f1c8e053d791ae2b4300ef5c9fe.jpg',
- 'https://i.pinimg.com/enabled/564x/fa/0a/c2/fa0ac2b7145af1205c87350f7c735683.jpg',
- 'https://i.pinimg.com/enabled/564x/94/84/57/9484579fcbf7e768d6206b07fa44c2b9.jpg',
- 'https://i.pinimg.com/enabled/564x/fd/30/bf/fd30bf62f6409129ce6538f1b9ed7b8b.jpg',
- 'https://i.pinimg.com/enabled/564x/44/b2/11/44b21104b4e41736c99ee183127aab3d.jpg',
- 'https://i.pinimg.com/enabled/564x/f7/ce/56/f7ce5629aa91866020a559ef7e249f1c.jpg',
- 'https://i.pinimg.com/enabled/564x/7b/ac/36/7bac368ff9b5f702d9b727491f8d4ef0.jpg',
- 'https://i.pinimg.com/enabled/564x/39/1a/51/391a514a013f62ca9f25f47b4cbd7776.jpg',
- 'https://i.pinimg.com/enabled/564x/03/d2/96/03d2967de5d249f88155cab461e69f3a.jpg'
+ "https://i.pinimg.com/enabled/564x/e2/5d/31/e25d3199f73c9453035727f8c7a70170.jpg",
+ "https://i.pinimg.com/enabled/564x/5f/ed/28/5fed282cff8d22ac857e2a489031d05a.jpg",
+ "https://i.pinimg.com/736x/05/a8/71/05a87162a78e2cad2ffe0a9eac6b4e2c.jpg",
+ "https://i.pinimg.com/736x/c6/ac/13/c6ac139ed02c9accd34dbb16d7466025.jpg",
+ "https://i.pinimg.com/736x/72/69/c3/7269c3d939764b024da9a6869dc59a0f.jpg",
+ "https://i.pinimg.com/enabled/564x/cc/5c/6f/cc5c6f1c8e053d791ae2b4300ef5c9fe.jpg",
+ "https://i.pinimg.com/enabled/564x/fa/0a/c2/fa0ac2b7145af1205c87350f7c735683.jpg",
+ "https://i.pinimg.com/enabled/564x/94/84/57/9484579fcbf7e768d6206b07fa44c2b9.jpg",
+ "https://i.pinimg.com/enabled/564x/fd/30/bf/fd30bf62f6409129ce6538f1b9ed7b8b.jpg",
+ "https://i.pinimg.com/enabled/564x/44/b2/11/44b21104b4e41736c99ee183127aab3d.jpg",
+ "https://i.pinimg.com/enabled/564x/f7/ce/56/f7ce5629aa91866020a559ef7e249f1c.jpg",
+ "https://i.pinimg.com/enabled/564x/7b/ac/36/7bac368ff9b5f702d9b727491f8d4ef0.jpg",
+ "https://i.pinimg.com/enabled/564x/39/1a/51/391a514a013f62ca9f25f47b4cbd7776.jpg",
+ "https://i.pinimg.com/enabled/564x/03/d2/96/03d2967de5d249f88155cab461e69f3a.jpg",
];
-// Constants
+/**
+ * @typedef {Object} Song
+ * @property {string} id - Unique identifier for the song
+ * @property {string} title - Song title
+ * @property {string} artist - Artist name
+ * @property {string} album - Album name
+ * @property {string} [artwork] - URL to artwork image
+ */
+
+/**
+ * @typedef {Object} UIElements
+ * @property {HTMLElement} playButton - Play/pause button
+ * @property {HTMLElement} prevButton - Previous track button
+ * @property {HTMLElement} nextButton - Next track button
+ * @property {HTMLElement} timeElapsed - Time elapsed display
+ * @property {HTMLElement} timeTotal - Total time display
+ * @property {HTMLElement} songCover - Album artwork
+ * @property {HTMLElement} songTitle - Song title display
+ * @property {HTMLElement} songArtistAlbum - Artist and album display
+ * @property {HTMLCanvasElement} visualizer - Audio visualizer canvas
+ */
+
+// Configuration
+const STORE_LIMIT = artworkCollection.length;
const SEEKBAR_CONFIG = {
HEIGHT: 4,
THUMB_RADIUS: 6,
@@ -28,70 +57,41 @@ const SEEKBAR_CONFIG = {
}
};
-const STORE_LIMIT = artworkCollection.length;
-const MIN_SCREEN_WIDTH = 2800;
-
-// Audio Context and Core Variables
-let audioContext;
-let sourceNode;
-let analyzerNode;
-let audioBuffer;
-let startTime;
-let pauseTime = 0;
-let isPlaying = false;
-let currentSong = null;
-let isLoading = true;
-let isDragging = false;
-
-// DOM Elements
-const elements = {
- playButton: document.getElementById('song-play'),
- prevButton: document.getElementById('song-prev'),
- nextButton: document.getElementById('song-next'),
- timeElapsed: document.getElementById('song-time-elapsed'),
- timeTotal: document.getElementById('song-time-total'),
- songCover: document.getElementById('song-cover'),
- songTitle: document.getElementById('song-title'),
- songArtistAlbum: document.getElementById('song-artist-album'),
- visualizer: document.getElementById('song-visualizer')
-};
-
-// Create and setup seekbar
-const seekbarCanvas = document.createElement('canvas');
-seekbarCanvas.id = 'custom-seekbar';
-seekbarCanvas.width = 140;
-seekbarCanvas.height = 20;
-seekbarCanvas.style.cssText = 'position: absolute; left: 30px; top: 220px; cursor: pointer; z-index: 1;';
-document.getElementById('song-time').parentNode.insertBefore(seekbarCanvas, document.getElementById('song-time'));
-
+/**
+ * Manages artwork selection and rotation for songs
+ */
class ArtworkManager {
- constructor() {
+ /**
+ * @param {string[]} artworkCollection - Collection of artwork URLs
+ */
+ constructor(artworkCollection) {
+ this.collection = artworkCollection;
this.usedArtwork = new Set();
this.availableArtwork = [...artworkCollection];
}
+ /**
+ * @returns {string} Random unused artwork URL
+ */
getRandomArtwork() {
- // If all artwork has been used, reset the pool
if (this.availableArtwork.length === 0) {
this.resetArtworkPool();
}
-
- // Get random artwork from available pool
const randomIndex = Math.floor(Math.random() * this.availableArtwork.length);
const artwork = this.availableArtwork[randomIndex];
-
- // Remove from available pool and add to used set
this.availableArtwork.splice(randomIndex, 1);
this.usedArtwork.add(artwork);
-
return artwork;
}
resetArtworkPool() {
- this.availableArtwork = [...artworkCollection];
+ this.availableArtwork = [...this.collection];
this.usedArtwork.clear();
}
+ /**
+ * @param {string} artwork - Artwork URL to release back to pool
+ */
releaseArtwork(artwork) {
if (this.usedArtwork.has(artwork)) {
this.usedArtwork.delete(artwork);
@@ -100,32 +100,39 @@ class ArtworkManager {
}
}
-// Song Store Management
+/**
+ * Manages song queue and persistence
+ */
class SongStore {
- constructor() {
+ /**
+ * @param {number} limit - Maximum number of songs to store
+ * @param {ArtworkManager} artworkManager - Artwork management instance
+ */
+ constructor(limit, artworkManager) {
+ this.limit = limit;
+ this.artworkManager = artworkManager;
this.songs = JSON.parse(localStorage.getItem('songStore')) || [];
this.currentIndex = parseInt(localStorage.getItem('currentSongIndex')) || -1;
this.artworkCache = JSON.parse(localStorage.getItem('artworkCache')) || {};
- this.artworkManager = new ArtworkManager();
// Restore artwork state
Object.values(this.artworkCache).forEach(artwork => {
this.artworkManager.usedArtwork.add(artwork);
});
-
- // Remove any artwork from availableArtwork that's already in use
this.artworkManager.availableArtwork = this.artworkManager.availableArtwork
.filter(artwork => !this.artworkManager.usedArtwork.has(artwork));
}
+ /**
+ * @param {Song} song - Song to add to store
+ * @returns {Promise<Song|null>} Added song with artwork
+ */
async addSong(song) {
if (!song) return null;
- // Generate unique artwork for the song
const artwork = this.artworkManager.getRandomArtwork();
this.artworkCache[song.id] = artwork;
- // Always add the song, even if it's a duplicate
this.songs.push({
id: song.id,
title: song.title,
@@ -133,10 +140,8 @@ class SongStore {
album: song.album
});
- // Maintain the store limit
- if (this.songs.length > STORE_LIMIT) {
+ if (this.songs.length > this.limit) {
const removedSong = this.songs.shift();
- // Release the artwork back to the pool
const removedArtwork = this.artworkCache[removedSong.id];
this.artworkManager.releaseArtwork(removedArtwork);
delete this.artworkCache[removedSong.id];
@@ -146,396 +151,593 @@ class SongStore {
this.currentIndex = this.songs.length - 1;
this.save();
- return {
- ...this.songs[this.currentIndex],
- artwork: this.artworkCache[song.id]
- };
+ return { ...song, artwork };
}
+ /**
+ * @returns {Promise<Song|null>} Next song in queue or new song
+ */
async getNext() {
if (this.currentIndex < this.songs.length - 1) {
this.currentIndex++;
this.save();
- return {
- ...this.songs[this.currentIndex],
- artwork: this.artworkCache[this.songs[this.currentIndex].id]
- };
+ return this._getSongWithArtwork(this.currentIndex);
}
- // Get new song if we're at the end
const nextSongId = this.songs[this.currentIndex]?.id;
- const newSong = await this.fetchNewSong(nextSongId);
+ const newSong = await this._fetchSong(nextSongId);
return this.addSong(newSong);
}
+ /**
+ * @returns {Promise<Song|null>} Previous song in queue
+ */
async getPrevious() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.save();
- return {
- ...this.songs[this.currentIndex],
- artwork: this.artworkCache[this.songs[this.currentIndex].id]
- };
+ return this._getSongWithArtwork(this.currentIndex);
}
return null;
}
- async fetchNewSong(nextSongId = null) {
+ /**
+ * @private
+ * @param {string} [nextSongId] - ID of current song for continuity
+ * @returns {Promise<Song|null>} Fetched song data
+ */
+ async _fetchSong(nextSongId = null) {
try {
- const url = nextSongId ? `/services/stream/random-song?next=${nextSongId}` : '/services/stream/random-song';
+ const url = nextSongId ?
+ `/services/stream/random-song?next=${nextSongId}` :
+ '/services/stream/random-song';
const response = await fetch(url);
- const data = await response.json();
- return data;
+ return await response.json();
} catch (error) {
- console.error('Error fetching new song:', error);
+ console.error('Error fetching song:', error);
return null;
}
}
- save() {
- localStorage.setItem('songStore', JSON.stringify(this.songs));
- localStorage.setItem('currentSongIndex', this.currentIndex.toString());
- localStorage.setItem('artworkCache', JSON.stringify(this.artworkCache));
+ /**
+ * @private
+ * @param {number} index - Index of song to retrieve
+ * @returns {Song|null} Song with artwork
+ */
+ _getSongWithArtwork(index) {
+ const song = this.songs[index];
+ return song ? { ...song, artwork: this.artworkCache[song.id] } : null;
}
getCurrentSong() {
- if (this.currentIndex >= 0) {
- const song = this.songs[this.currentIndex];
- return {
- ...song,
- artwork: this.artworkCache[song.id]
- };
- }
- return null;
+ return this.currentIndex >= 0 ? this._getSongWithArtwork(this.currentIndex) : null;
}
getArtwork(songId) {
return this.artworkCache[songId];
}
-}
-
-const songStore = new SongStore();
-// Audio Control Functions
-async function initAudio() {
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
- analyzerNode = audioContext.createAnalyser();
- analyzerNode.fftSize = 256;
- analyzerNode.connect(audioContext.destination);
+ save() {
+ localStorage.setItem('songStore', JSON.stringify(this.songs));
+ localStorage.setItem('currentSongIndex', this.currentIndex.toString());
+ localStorage.setItem('artworkCache', JSON.stringify(this.artworkCache));
+ }
}
-function formatTime(time) {
- const minutes = Math.floor(time / 60);
- const seconds = Math.floor(time % 60).toString().padStart(2, '0');
- return `${minutes}:${seconds}`;
-}
+/**
+ * Manages audio playback and visualization
+ */
+class AudioPlayer {
+ /**
+ * @param {UIElements} elements - DOM elements
+ * @param {SongStore} songStore - Song management instance
+ */
+ constructor(elements, songStore) {
+ this.elements = elements;
+ this.songStore = songStore;
+ this.audioContext = null;
+ this.sourceNode = null;
+ this.analyzerNode = null;
+ this.audioBuffer = null;
+ this.startTime = 0;
+ this.pauseTime = 0;
+ this.isPlaying = false;
+ this.isLoading = true;
+ this.isDragging = false;
+ this.currentSong = null;
+
+ this.setupSeekbar();
+ this.bindMethods();
+ }
-async function fetchAudio(url) {
- isLoading = true;
- updateControls();
- try {
- const response = await fetch(url);
- const arrayBuffer = await response.arrayBuffer();
- audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
- elements.timeTotal.textContent = formatTime(audioBuffer.duration);
- isLoading = false;
- updateControls();
- } catch (error) {
- console.error('Error loading audio:', error);
- isLoading = false;
- updateControls();
+ /**
+ * @private
+ */
+ setupSeekbar() {
+ this.seekbarCanvas = document.createElement('canvas');
+ this.seekbarCanvas.id = 'custom-seekbar';
+ this.seekbarCanvas.width = 140;
+ this.seekbarCanvas.height = 20;
+ this.seekbarCanvas.style.cssText = 'position: absolute; left: 30px; top: 220px; cursor: pointer; z-index: 1;';
+ document.getElementById('song-time').parentNode.insertBefore(
+ this.seekbarCanvas,
+ document.getElementById('song-time')
+ );
}
-}
-function playAudio(offset = 0) {
- if (!audioBuffer) return;
- if (isPlaying) stopAudio();
+ /**
+ * @private
+ */
+ bindMethods() {
+ this.handleSeek = (position) => {
+ if (!this.audioBuffer) return;
+ const seekTime = (position / this.seekbarCanvas.width) * this.audioBuffer.duration;
+ this.seek(seekTime);
+ };
+
+ this.handlePlayPause = () => {
+ if (this.isLoading) return;
+ if (this.isPlaying) {
+ this.stop();
+ this.pauseTime = this.audioContext.currentTime - this.startTime;
+ } else {
+ this.play(this.pauseTime);
+ }
+ this.saveState();
+ };
+
+ this.handlePrevious = () => this.loadNewSong(this.isPlaying, 'previous');
+ this.handleNext = () => this.loadNewSong(this.isPlaying, 'next');
+
+ this.handleVisibilityChange = () => {
+ if (document.hidden) {
+ this.saveState();
+ }
+ };
- offset = Math.min(Math.max(0, offset), audioBuffer.duration);
+ this.handleBeforeUnload = () => {
+ this.saveState();
+ };
- sourceNode = audioContext.createBufferSource();
- sourceNode.buffer = audioBuffer;
- sourceNode.connect(analyzerNode);
+ this.update = () => {
+ if (this.audioBuffer && this.isPlaying) {
+ const currentTime = this.audioContext.currentTime - this.startTime;
+ if (currentTime >= this.audioBuffer.duration) {
+ this.loadNewSong(true, 'next');
+ return;
+ }
+ }
+ this.updateTimeDisplay();
+ this.drawSeekbar();
+ requestAnimationFrame(this.update);
+ };
+ }
+
+ /**
+ * Initializes audio context and event listeners
+ */
+ async init() {
+ await this.initAudioContext();
+ this.setupEventListeners();
+ await this.restoreState();
+ setInterval(() => this.saveState(), 500);
+ requestAnimationFrame(this.update);
+ }
- sourceNode.onended = async () => {
- const currentTime = audioContext.currentTime - startTime;
- if (currentTime >= audioBuffer.duration - 0.1) {
- await loadNewSong(true, 'next');
+ /**
+ * @private
+ */
+ async initAudioContext() {
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ this.analyzerNode = this.audioContext.createAnalyser();
+ this.analyzerNode.fftSize = 256;
+ this.analyzerNode.connect(this.audioContext.destination);
+ }
+
+ /**
+ * @private
+ * @param {string} url - Audio file URL
+ */
+ async loadAudio(url) {
+ this.isLoading = true;
+ this.updateControls();
+ try {
+ const response = await fetch(url);
+ const arrayBuffer = await response.arrayBuffer();
+ this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
+ this.elements.timeTotal.textContent = this.formatTime(this.audioBuffer.duration);
+ } catch (error) {
+ console.error('Error loading audio:', error);
+ } finally {
+ this.isLoading = false;
+ this.updateControls();
}
- };
+ }
- sourceNode.start(0, offset);
- startTime = audioContext.currentTime - offset;
- isPlaying = true;
- updateUI();
-}
+ /**
+ * @private
+ * @param {number} [offset=0] - Start offset in seconds
+ */
+ play(offset = 0) {
+ if (!this.audioBuffer) return;
+ if (this.isPlaying) this.stop();
+
+ offset = Math.min(Math.max(0, offset), this.audioBuffer.duration);
-function stopAudio() {
- if (sourceNode) {
- sourceNode.stop();
- sourceNode = null;
- isPlaying = false;
- updateUI();
+ this.sourceNode = this.audioContext.createBufferSource();
+ this.sourceNode.buffer = this.audioBuffer;
+ this.sourceNode.connect(this.analyzerNode);
+
+ this.sourceNode.onended = async () => {
+ const currentTime = this.audioContext.currentTime - this.startTime;
+ if (currentTime >= this.audioBuffer.duration - 0.1) {
+ await this.loadNewSong(true, 'next');
+ }
+ };
+
+ this.sourceNode.start(0, offset);
+ this.startTime = this.audioContext.currentTime - offset;
+ this.isPlaying = true;
+ this.updateUI();
}
-}
-function seekAudio(time) {
- if (!audioBuffer) return;
- const wasPlaying = isPlaying;
- stopAudio();
- pauseTime = time;
- if (wasPlaying) {
- playAudio(pauseTime);
+ /**
+ * Stops audio playback
+ * @private
+ */
+ stop() {
+ if (this.sourceNode) {
+ this.sourceNode.stop();
+ this.sourceNode = null;
+ this.isPlaying = false;
+ this.updateUI();
+ }
}
- drawSeekbar();
- savePlaybackState();
-}
-// UI Update Functions
-function updateUI() {
- elements.playButton.innerHTML = isPlaying ? "&#10074;&#10074;" : "&#9658;";
- drawVisualizer();
- updateTimeDisplay();
- drawSeekbar();
-}
+ /**
+ * Seeks to specific time in audio
+ * @private
+ * @param {number} time - Time in seconds to seek to
+ */
+ seek(time) {
+ if (!this.audioBuffer) return;
+ const wasPlaying = this.isPlaying;
+ this.stop();
+ this.pauseTime = time;
+ if (wasPlaying) {
+ this.play(this.pauseTime);
+ }
+ this.drawSeekbar();
+ this.saveState();
+ }
-function updateControls() {
- elements.playButton.disabled = isLoading;
- elements.prevButton.disabled = isLoading;
- elements.nextButton.disabled = isLoading;
- if (isLoading) stopAudio();
-}
+ /**
+ * Updates all UI elements
+ * @private
+ */
+ updateUI() {
+ this.elements.playButton.innerHTML = this.isPlaying ? "&#10074;&#10074;" : "&#9658;";
+ this.drawVisualizer();
+ this.updateTimeDisplay();
+ this.drawSeekbar();
+ }
-function updateSongInfo() {
- if (currentSong) {
- elements.songTitle.textContent = currentSong.title;
- elements.songArtistAlbum.textContent = `${currentSong.artist} - ${currentSong.album}`;
- // Always update artwork with the cached version or generate new
- elements.songCover.src = currentSong.artwork || songStore.getArtwork(currentSong.id);
+ /**
+ * Updates control button states
+ * @private
+ */
+ updateControls() {
+ this.elements.playButton.disabled = this.isLoading;
+ this.elements.prevButton.disabled = this.isLoading;
+ this.elements.nextButton.disabled = this.isLoading;
+ if (this.isLoading) this.stop();
}
-}
-// Canvas Drawing Functions
-function drawSeekbar() {
- if (!audioBuffer) return;
+ /**
+ * Updates song information display
+ * @private
+ */
+ updateSongInfo() {
+ if (this.currentSong) {
+ this.elements.songTitle.textContent = this.currentSong.title;
+ this.elements.songArtistAlbum.textContent =
+ `${this.currentSong.artist} - ${this.currentSong.album}`;
+ this.elements.songCover.src =
+ this.currentSong.artwork || this.songStore.getArtwork(this.currentSong.id);
+ }
+ }
- const ctx = seekbarCanvas.getContext('2d');
- const { width, height } = seekbarCanvas;
- const centerY = height / 2;
+ /**
+ * Draws seekbar with current progress
+ * @private
+ */
+ drawSeekbar() {
+ if (!this.audioBuffer) return;
- ctx.clearRect(0, 0, width, height);
+ const ctx = this.seekbarCanvas.getContext('2d');
+ const { width, height } = this.seekbarCanvas;
+ const centerY = height / 2;
- // Background bar
- ctx.fillStyle = SEEKBAR_CONFIG.COLORS.BASE;
- ctx.fillRect(0, centerY - SEEKBAR_CONFIG.HEIGHT / 2, width, SEEKBAR_CONFIG.HEIGHT);
+ ctx.clearRect(0, 0, width, height);
- // Progress bar
- const currentTime = isPlaying ? audioContext.currentTime - startTime : pauseTime;
- const progress = (currentTime / audioBuffer.duration) * width;
+ // Background bar
+ ctx.fillStyle = SEEKBAR_CONFIG.COLORS.BASE;
+ ctx.fillRect(0, centerY - SEEKBAR_CONFIG.HEIGHT / 2, width, SEEKBAR_CONFIG.HEIGHT);
- ctx.fillStyle = SEEKBAR_CONFIG.COLORS.PROGRESS;
- ctx.fillRect(0, centerY - SEEKBAR_CONFIG.HEIGHT / 2, progress, SEEKBAR_CONFIG.HEIGHT);
+ // Progress bar
+ const currentTime = this.isPlaying ?
+ this.audioContext.currentTime - this.startTime : this.pauseTime;
+ const progress = (currentTime / this.audioBuffer.duration) * width;
- // Thumb
- ctx.beginPath();
- ctx.arc(progress, centerY, SEEKBAR_CONFIG.THUMB_RADIUS, 0, Math.PI * 2);
- ctx.fill();
-}
+ ctx.fillStyle = SEEKBAR_CONFIG.COLORS.PROGRESS;
+ ctx.fillRect(0, centerY - SEEKBAR_CONFIG.HEIGHT / 2, progress, SEEKBAR_CONFIG.HEIGHT);
-function drawVisualizer() {
- if (!isPlaying) return;
+ // Thumb
+ ctx.beginPath();
+ ctx.arc(progress, centerY, SEEKBAR_CONFIG.THUMB_RADIUS, 0, Math.PI * 2);
+ ctx.fill();
+ }
- const ctx = elements.visualizer.getContext('2d');
- const bufferLength = analyzerNode.frequencyBinCount;
- const dataArray = new Uint8Array(bufferLength);
+ /**
+ * Draws audio visualization
+ * @private
+ */
+ drawVisualizer() {
+ if (!this.isPlaying) return;
- function animate() {
- if (!isPlaying) return;
- requestAnimationFrame(animate);
+ const ctx = this.elements.visualizer.getContext('2d');
+ const bufferLength = this.analyzerNode.frequencyBinCount;
+ const dataArray = new Uint8Array(bufferLength);
- analyzerNode.getByteFrequencyData(dataArray);
- ctx.clearRect(0, 0, elements.visualizer.width, elements.visualizer.height);
+ const animate = () => {
+ if (!this.isPlaying) return;
+ requestAnimationFrame(animate);
- const barWidth = (elements.visualizer.width / bufferLength) * 2.5;
- let x = 0;
+ this.analyzerNode.getByteFrequencyData(dataArray);
+ ctx.clearRect(0, 0, this.elements.visualizer.width, this.elements.visualizer.height);
- for (let i = 0; i < bufferLength; i++) {
- const barHeight = (dataArray[i] / 255) * elements.visualizer.height;
- ctx.fillStyle = `rgb(${dataArray[i]}, 50, 255)`;
- ctx.fillRect(x, elements.visualizer.height - barHeight, barWidth, barHeight);
- x += barWidth + 1;
- }
+ const barWidth = (this.elements.visualizer.width / bufferLength) * 2.5;
+ let x = 0;
+
+ for (let i = 0; i < bufferLength; i++) {
+ const barHeight = (dataArray[i] / 255) * this.elements.visualizer.height;
+ ctx.fillStyle = `rgb(${dataArray[i]}, 50, 255)`;
+ ctx.fillRect(x, this.elements.visualizer.height - barHeight, barWidth, barHeight);
+ x += barWidth + 1;
+ }
+ };
+
+ animate();
}
- animate();
-}
+ /**
+ * Saves current playback state
+ * @private
+ */
+ saveState() {
+ if (!this.currentSong) return;
+
+ const currentTime = this.isPlaying ?
+ this.audioContext.currentTime - this.startTime : this.pauseTime;
+ const state = {
+ songId: this.currentSong.id,
+ timeStamp: Math.min(currentTime, this.audioBuffer?.duration || 0),
+ isPlaying: this.isPlaying,
+ artwork: this.elements.songCover.src,
+ songTitle: this.currentSong.title,
+ songArtist: this.currentSong.artist,
+ songAlbum: this.currentSong.album
+ };
-// State Management Functions
-function savePlaybackState() {
- if (!currentSong) return;
-
- const currentTime = isPlaying ? audioContext.currentTime - startTime : pauseTime;
- const state = {
- songId: currentSong.id,
- timeStamp: Math.min(currentTime, audioBuffer?.duration || 0),
- isPlaying,
- artwork: elements.songCover.src,
- songTitle: currentSong.title,
- songArtist: currentSong.artist,
- songAlbum: currentSong.album
- };
-
- localStorage.setItem('playbackState', JSON.stringify(state));
-}
+ localStorage.setItem('playbackState', JSON.stringify(state));
+ }
-// Song Loading and Navigation
-async function loadNewSong(autoplay = false, direction = 'next') {
- try {
- const wasPlaying = isPlaying || autoplay;
- stopAudio();
+ /**
+ * Loads and plays a new song
+ * @private
+ * @param {boolean} autoplay - Whether to start playing immediately
+ * @param {'next'|'previous'} direction - Direction to load song from
+ */
+ async loadNewSong(autoplay = false, direction = 'next') {
+ try {
+ const wasPlaying = this.isPlaying || autoplay;
+ this.stop();
- let nextSong;
- if (direction === 'next') {
- nextSong = await songStore.getNext();
- } else {
- nextSong = await songStore.getPrevious();
- if (!nextSong) return; // Don't proceed if no previous song
- }
+ const nextSong = direction === 'next' ?
+ await this.songStore.getNext() :
+ await this.songStore.getPrevious();
- currentSong = nextSong;
- updateSongInfo(); // This will now use the cached artwork
+ if (!nextSong && direction === 'previous') return;
- await fetchAudio(`/services/stream/song/${currentSong.id}`);
- pauseTime = 0; // Reset seek position for new song
+ this.currentSong = nextSong;
+ this.updateSongInfo();
- if (wasPlaying) {
- playAudio(0);
+ await this.loadAudio(`/services/stream/song/${this.currentSong.id}`);
+ this.pauseTime = 0;
+
+ if (wasPlaying) {
+ this.play(0);
+ }
+
+ this.saveState();
+ } catch (error) {
+ console.error('Error loading song:', error);
}
+ }
+
+ /**
+ * Sets up all event listeners
+ * @private
+ */
+ setupEventListeners() {
+ this.seekbarCanvas.addEventListener('mousedown', (e) => {
+ this.isDragging = true;
+ const rect = this.seekbarCanvas.getBoundingClientRect();
+ const position = Math.max(0, Math.min((e.clientX - rect.left), this.seekbarCanvas.width));
+ this.handleSeek(position);
+ });
+
+ this.seekbarCanvas.addEventListener('mousemove', (e) => {
+ if (this.isDragging) {
+ const rect = this.seekbarCanvas.getBoundingClientRect();
+ const position = Math.max(0, Math.min((e.clientX - rect.left), this.seekbarCanvas.width));
+ this.handleSeek(position);
+ }
+ });
+
+ this.seekbarCanvas.addEventListener('mouseup', () => this.isDragging = false);
+ this.seekbarCanvas.addEventListener('mouseleave', () => this.isDragging = false);
+ document.addEventListener('mouseup', () => this.isDragging = false);
+
+ this.elements.playButton.addEventListener('click', this.handlePlayPause);
+ this.elements.prevButton.addEventListener('click', () => this.loadNewSong(this.isPlaying, 'previous'));
+ this.elements.nextButton.addEventListener('click', () => this.loadNewSong(this.isPlaying, 'next'));
+
+ this.elements.songCover.addEventListener('error', () => {
+ this.elements.songCover.src = this.songStore.getArtwork(this.currentSong.id);
+ this.saveState();
+ });
- savePlaybackState();
- } catch (error) {
- console.error('Error loading song:', error);
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ window.addEventListener('beforeunload', this.handleBeforeUnload);
}
-}
-function getCurrentScale() {
- const screenWidth = window.innerWidth;
- return screenWidth <= MIN_SCREEN_WIDTH ? 1 : screenWidth / MIN_SCREEN_WIDTH;
-}
+ /**
+ * Handles seek bar interaction
+ * @private
+ * @param {number} position - Position in pixels on seekbar
+ */
+ handleSeek(position) {
+ if (!this.audioBuffer) return;
+ const seekTime = (position / this.seekbarCanvas.width) * this.audioBuffer.duration;
+ this.seek(seekTime);
+ }
-// Event Listeners
-function setupEventListeners() {
- seekbarCanvas.addEventListener('mousedown', (e) => {
- isDragging = true;
- const scale = getCurrentScale();
- const rect = seekbarCanvas.getBoundingClientRect();
- // Adjust position calculation for scale
- const position = Math.max(0, Math.min((e.clientX - rect.left) / scale, seekbarCanvas.width));
- handleSeek(position);
- });
-
- seekbarCanvas.addEventListener('mousemove', (e) => {
- if (isDragging) {
- const scale = getCurrentScale();
- const rect = seekbarCanvas.getBoundingClientRect();
- // Adjust position calculation for scale
- const position = Math.max(0, Math.min((e.clientX - rect.left) / scale, seekbarCanvas.width));
- handleSeek(position);
- }
- });
-
- seekbarCanvas.addEventListener('mouseup', () => isDragging = false);
- seekbarCanvas.addEventListener('mouseleave', () => isDragging = false);
- document.addEventListener('mouseup', () => isDragging = false);
-
- // Rest of the event listeners remain the same...
- elements.playButton.addEventListener('click', () => {
- if (isLoading) return;
- if (isPlaying) {
- stopAudio();
- pauseTime = audioContext.currentTime - startTime;
+ /**
+ * Handles play/pause button click
+ * @private
+ */
+ handlePlayPause() {
+ if (this.isLoading) return;
+ if (this.isPlaying) {
+ this.stop();
+ this.pauseTime = this.audioContext.currentTime - this.startTime;
} else {
- playAudio(pauseTime);
+ this.play(this.pauseTime);
}
- savePlaybackState();
- });
-
- elements.prevButton.addEventListener('click', () => loadNewSong(isPlaying, 'previous'));
- elements.nextButton.addEventListener('click', () => loadNewSong(isPlaying, 'next'));
+ this.saveState();
+ }
- elements.songCover.addEventListener('error', () => {
- elements.songCover.src = songStore.getArtwork(currentSong.id);
- savePlaybackState();
- });
+ /**
+ * Handles visibility change
+ * @private
+ */
+ handleVisibilityChange() {
+ if (document.hidden) {
+ this.saveState();
+ }
+ }
- document.addEventListener('visibilitychange', savePlaybackState);
- window.addEventListener('beforeunload', savePlaybackState);
-}
+ /**
+ * Handles page unload
+ * @private
+ */
+ handleBeforeUnload() {
+ this.saveState();
+ }
-function handleSeek(position) {
- if (!audioBuffer) return;
- const seekTime = (position / seekbarCanvas.width) * audioBuffer.duration;
- seekAudio(seekTime);
-}
+ /**
+ * Updates time display
+ * @private
+ */
+ updateTimeDisplay() {
+ if (!this.audioBuffer) return;
+ const currentTime = this.isPlaying ?
+ this.audioContext.currentTime - this.startTime : this.pauseTime;
+ this.elements.timeElapsed.textContent = this.formatTime(currentTime);
+ this.elements.timeTotal.textContent = this.formatTime(this.audioBuffer.duration);
+ }
-function updateTimeDisplay() {
- if (!audioBuffer) return;
- const currentTime = isPlaying ? audioContext.currentTime - startTime : pauseTime;
- elements.timeElapsed.textContent = formatTime(currentTime);
- elements.timeTotal.textContent = formatTime(audioBuffer.duration);
-}
+ /**
+ * Formats time in seconds to MM:SS format
+ * @private
+ * @param {number} time - Time in seconds
+ * @returns {string} Formatted time string
+ */
+ formatTime(time) {
+ const minutes = Math.floor(time / 60);
+ const seconds = Math.floor(time % 60).toString().padStart(2, '0');
+ return `${minutes}:${seconds}`;
+ }
-// Main update loop
-function update() {
- if (audioBuffer && isPlaying) {
- const currentTime = audioContext.currentTime - startTime;
- if (currentTime >= audioBuffer.duration) {
- loadNewSong(true, 'next');
- return;
+ /**
+ * Main update loop
+ * @private
+ */
+ update() {
+ if (this.audioBuffer && this.isPlaying) {
+ const currentTime = this.audioContext.currentTime - this.startTime;
+ if (currentTime >= this.audioBuffer.duration) {
+ this.loadNewSong(true, 'next');
+ return;
+ }
}
- }
- updateTimeDisplay();
- drawSeekbar();
- requestAnimationFrame(update);
-}
+ this.updateTimeDisplay();
+ this.drawSeekbar();
+ requestAnimationFrame(this.update);
+ }
-// Initialization
-async function init() {
- await initAudio();
- setupEventListeners();
-
- try {
- const savedState = localStorage.getItem('playbackState');
- if (savedState) {
- const state = JSON.parse(savedState);
- currentSong = {
- id: state.songId,
- title: state.songTitle,
- artist: state.songArtist,
- album: state.songAlbum,
- artwork: state.artwork
- };
-
- updateSongInfo();
- await fetchAudio(`/services/stream/song/${state.songId}`);
- pauseTime = state.timeStamp || 0;
-
- if (state.isPlaying) {
- setTimeout(() => playAudio(pauseTime), 100);
+ /**
+ * Restores previous playback state
+ * @private
+ */
+ async restoreState() {
+ try {
+ const savedState = localStorage.getItem('playbackState');
+ if (savedState) {
+ const state = JSON.parse(savedState);
+ this.currentSong = {
+ id: state.songId,
+ title: state.songTitle,
+ artist: state.songArtist,
+ album: state.songAlbum,
+ artwork: state.artwork
+ };
+
+ this.updateSongInfo();
+ await this.loadAudio(`/services/stream/song/${state.songId}`);
+ this.pauseTime = state.timeStamp || 0;
+
+ if (state.isPlaying) {
+ setTimeout(() => this.play(this.pauseTime), 100);
+ } else {
+ this.drawSeekbar();
+ this.updateTimeDisplay();
+ }
} else {
- drawSeekbar();
- updateTimeDisplay();
+ await this.loadNewSong(false);
}
- } else {
- await loadNewSong(false);
+ } catch (error) {
+ console.error('Error restoring state:', error);
+ await this.loadNewSong(false);
}
- } catch (error) {
- console.error('Error restoring state:', error);
- await loadNewSong(false);
}
-
- setInterval(savePlaybackState, 500);
- update();
}
-init();
+// Initialize player
+const elements = {
+ playButton: document.getElementById('song-play'),
+ prevButton: document.getElementById('song-prev'),
+ nextButton: document.getElementById('song-next'),
+ timeElapsed: document.getElementById('song-time-elapsed'),
+ timeTotal: document.getElementById('song-time-total'),
+ songCover: document.getElementById('song-cover'),
+ songTitle: document.getElementById('song-title'),
+ songArtistAlbum: document.getElementById('song-artist-album'),
+ visualizer: document.getElementById('song-visualizer')
+};
+
+const artworkManager = new ArtworkManager(artworkCollection);
+const songStore = new SongStore(STORE_LIMIT, artworkManager);
+const player = new AudioPlayer(elements, songStore);
+player.init();
diff --git a/static/js/shared/resolutionScaling.js b/static/js/shared/resolutionScaling.js
deleted file mode 100644
index f11bc9a8..00000000
--- a/static/js/shared/resolutionScaling.js
+++ /dev/null
@@ -1,136 +0,0 @@
-(function () {
- // Core configuration constants
- const SITE_BASE_WIDTH = 1000;
- const MIN_SCALE_WIDTH = 1600; // Threshold for starting scale adjustments
- const HI_RES_WIDTH = 1920; // Threshold for forcing maximum resolution
- const MAX_DENSITY = 3; // Maximum supported image density
-
- // Calculate appropriate image density based on screen size and device
- function getTargetDensity() {
- const screenWidth = window.innerWidth;
- const devicePixelRatio = window.devicePixelRatio || 1;
-
- if (screenWidth < MIN_SCALE_WIDTH) return 1;
- if (screenWidth >= HI_RES_WIDTH) return MAX_DENSITY;
-
- const scaleRatio = screenWidth / MIN_SCALE_WIDTH;
- const effectiveScale = scaleRatio * devicePixelRatio;
-
- return effectiveScale > 2 ? 2 : 1;
- }
-
- // Update single image to appropriate resolution
- function forceHighResImage(img) {
- const srcset = img.getAttribute('srcset');
- if (!srcset) return;
-
- const sources = srcset.split(',').map(src => {
- const [url, size] = src.trim().split(' ');
- return {
- url: url,
- density: parseFloat(size.replace('x', '')) || 1
- };
- }).sort((a, b) => b.density - a.density);
-
- const targetDensity = getTargetDensity();
- const targetSource = sources.reduce((prev, curr) => {
- if (curr.density <= targetDensity && curr.density > prev.density) {
- return curr;
- }
- return prev;
- }, { density: 0 });
-
- if (img.src !== targetSource.url) {
- const newImg = document.createElement('img');
- Array.from(img.attributes).forEach(attr => {
- if (attr.name !== 'src' && attr.name !== 'srcset') {
- newImg.setAttribute(attr.name, attr.value);
- }
- });
- newImg.src = targetSource.url;
- img.parentNode.replaceChild(newImg, img);
- }
- }
-
- // Update all images on the page
- function updateAllImages() {
- document.querySelectorAll('img[srcset]').forEach(forceHighResImage);
- }
-
- // Calculate and apply scaling based on screen width
- function calculateScale() {
- const screenWidth = window.innerWidth;
- const wrapper = document.getElementById('body-wrapper');
- if (!wrapper) return;
-
- if (screenWidth <= MIN_SCALE_WIDTH) {
- wrapper.style.transform = 'scale(1)';
- wrapper.style.transformOrigin = 'left top';
- wrapper.style.left = '50%';
- wrapper.style.marginLeft = `-${SITE_BASE_WIDTH / 2}px`;
- return;
- }
-
- const scaleRatio = screenWidth / MIN_SCALE_WIDTH;
- const scaledWidth = SITE_BASE_WIDTH * scaleRatio;
- const leftPosition = (screenWidth - scaledWidth) / 2;
-
- wrapper.style.transform = `scale(${scaleRatio})`;
- wrapper.style.transformOrigin = 'left top';
- wrapper.style.left = `${leftPosition}px`;
- wrapper.style.position = 'absolute';
- wrapper.style.marginLeft = '0';
- }
-
- // Initialize wrapper positioning and initial calculations
- function init() {
- const wrapper = document.getElementById('body-wrapper');
- if (wrapper) {
- wrapper.style.position = 'absolute';
- wrapper.style.width = `${SITE_BASE_WIDTH}px`;
- wrapper.style.top = '0';
- }
-
- calculateScale();
- updateAllImages();
- }
-
- // Handle dynamically added images
- const observer = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === 1) {
- if (node.tagName === 'IMG' && node.hasAttribute('srcset')) {
- forceHighResImage(node);
- }
- node.querySelectorAll('img[srcset]').forEach(forceHighResImage);
- }
- });
- });
- });
-
- // Debounced resize handler
- let resizeTimeout;
- function handleResize() {
- clearTimeout(resizeTimeout);
- resizeTimeout = setTimeout(() => {
- calculateScale();
- updateAllImages();
- }, 100);
- }
-
- // Event listeners and initialization
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => {
- init();
- observer.observe(document.body, { childList: true, subtree: true });
- });
- } else {
- init();
- observer.observe(document.body, { childList: true, subtree: true });
- }
-
- window.addEventListener('load', init);
- window.addEventListener('resize', handleResize);
- window.addEventListener('orientationchange', handleResize);
-})(); \ No newline at end of file