diff options
| author | Bobby <[email protected]> | 2024-12-15 09:50:35 -0500 |
|---|---|---|
| committer | Bobby <[email protected]> | 2024-12-15 09:50:35 -0500 |
| commit | 06f23e7745c5875039720468db0a2786d2aaf848 (patch) | |
| tree | cc49ba62e71c35b3294edc7b753b9627b2d2ac9c /static | |
| parent | 2efc3e9fbb38e447c5e336dfea679644ea16af12 (diff) | |
| download | thatcomputerscientist-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.css | 43 | ||||
| -rw-r--r-- | static/css/shared/core.css | 10 | ||||
| -rw-r--r-- | static/css/styles.css | 20 | ||||
| -rw-r--r-- | static/images/core/backgrounds/announcements_border.jpg | bin | 0 -> 44000 bytes | |||
| -rw-r--r-- | static/images/core/gifs/hand.gif (renamed from static/images/gifs/hand.gif) | bin | 1630 -> 1630 bytes | |||
| -rw-r--r-- | static/images/core/gifs/new_announcement.gif (renamed from static/images/gifs/new_announcement.gif) | bin | 322 -> 322 bytes | |||
| -rw-r--r-- | static/images/core/gifs/update.gif | bin | 0 -> 6109 bytes | |||
| -rw-r--r-- | static/js/libs/marquee.js | 357 | ||||
| -rw-r--r-- | static/js/libs/pamphlet.js | 53 | ||||
| -rw-r--r-- | static/js/shared/kawaiiBeatsPlayer.js | 938 | ||||
| -rw-r--r-- | static/js/shared/resolutionScaling.js | 136 |
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 Binary files differnew file mode 100644 index 00000000..0fee97f4 --- /dev/null +++ b/static/images/core/backgrounds/announcements_border.jpg diff --git a/static/images/gifs/hand.gif b/static/images/core/gifs/hand.gif Binary files differindex 70d574cd..70d574cd 100644 --- a/static/images/gifs/hand.gif +++ b/static/images/core/gifs/hand.gif diff --git a/static/images/gifs/new_announcement.gif b/static/images/core/gifs/new_announcement.gif Binary files differindex ffef772d..ffef772d 100644 --- a/static/images/gifs/new_announcement.gif +++ b/static/images/core/gifs/new_announcement.gif diff --git a/static/images/core/gifs/update.gif b/static/images/core/gifs/update.gif Binary files differnew file mode 100644 index 00000000..17bf905b --- /dev/null +++ b/static/images/core/gifs/update.gif 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 ? "❚❚" : "►"; - 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 ? "❚❚" : "►"; + 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 |
