diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 95686c4..53f3baa 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -94,6 +94,32 @@ --rau-brand-text: white; } + .marquee { + overflow: hidden; + white-space: nowrap; + box-sizing: border-box; + position: relative; + } + + .marquee span { + display: inline-block; + position: absolute; + animation: marquee 5s linear infinite; + } + + @keyframes marquee { + from { + transform: translateX(100%); + } + to { + transform: translateX(-100%); + } + } + + .marquee-active span { + animation: marquee linear infinite; + } + .flash-error-bg, .flash-alert-bg { @apply bg-red-600; diff --git a/app/controllers/player_controller.rb b/app/controllers/player_controller.rb index f5d886f..a549c8f 100644 --- a/app/controllers/player_controller.rb +++ b/app/controllers/player_controller.rb @@ -16,11 +16,18 @@ def show if params[:t] render turbo_stream: [ + turbo_stream.update( - "track-info-wrapper", - partial: "track_info", + "player-frame", + partial: "player", locals: {track: @track} ) + + #turbo_stream.update( + # "track-info-wrapper", + # partial: "track_info", + # locals: {track: @track} + #) ] end end diff --git a/app/javascript/controllers/audio_player_controller.js b/app/javascript/controllers/audio_player_controller.js new file mode 100644 index 0000000..b8548fe --- /dev/null +++ b/app/javascript/controllers/audio_player_controller.js @@ -0,0 +1,221 @@ +import { Controller } from "@hotwired/stimulus"; +import { get } from '@rails/request.js' + +export default class extends Controller { + static targets = [ + "audio", + "playButton", + "progress", + "currentTime", + "duration", + "playIcon", + "pauseIcon", + "volumeSlider", + "volumeOnIcon", + "volumeOffIcon", + "sidebar" + ]; + + connect() { + this.audio = this.audioTarget; + this.playButton = this.playButtonTarget; + this.progress = this.progressTarget; + this.currentTime = this.currentTimeTarget; + this.duration = this.durationTarget; + this.volumeSlider = this.volumeSliderTarget; + this.volumeOnIcon = this.volumeOnIconTarget; + this.volumeOffIcon = this.volumeOffIconTarget; + + this.audio.addEventListener("timeupdate", this.updateProgress.bind(this)); + this.audio.addEventListener("loadedmetadata", this.updateDuration.bind(this)); + + // Automatically play the audio when it's loaded + this.audio.addEventListener("loadeddata", this.autoPlay.bind(this)); + + // Initialize volume level + this.volumeSlider.value = window.store.getState().volume || this.audio.volume; + + // Initialize halfway tracking + this.hasHalfwayEventFired = false; + } + + autoPlay() { + // Show loading animation (optional) + const playPromise = this.audio.play(); + + if (playPromise !== undefined) { + playPromise + .then(() => { + // Automatic playback started! + this.playIconTarget.classList.toggle("hidden") + this.pauseIconTarget.classList.toggle("hidden") + }) + .catch((error) => { + // Auto-play was prevented + console.error("Playback failed:", error); + //this.playButton.textContent = "Play"; + this.playIconTarget.classList.toggle("hidden") + this.pauseIconTarget.classList.toggle("hidden") + }); + } + } + + playPause() { + if (this.audio.paused) { + this.audio.play(); + //this.playButton.textContent = "Pause"; + this.playIconTarget.classList.toggle("hidden") + this.pauseIconTarget.classList.toggle("hidden") + } else { + this.audio.pause(); + //this.playButton.textContent = "Play"; + this.playIconTarget.classList.toggle("hidden") + this.pauseIconTarget.classList.toggle("hidden") + } + } + + setVolume() { + this.audio.volume = this.volumeSlider.value; + window.store.setState({ volume: this.volumeSlider.value }) + // Toggle volume icon + if (this.audio.volume === 0) { + this.volumeOnIcon.classList.add("hidden"); + this.volumeOffIcon.classList.remove("hidden"); + } else { + this.volumeOnIcon.classList.remove("hidden"); + this.volumeOffIcon.classList.add("hidden"); + } + } + + toggleMute() { + if (this.audio.volume > 0) { + this.audio.volume = 0; + window.store.setState({ volume: 0 }) + this.volumeSlider.value = 0; + this.volumeOnIcon.classList.add("hidden"); + this.volumeOffIcon.classList.remove("hidden"); + } else { + this.audio.volume = 1; + window.store.setState({ volume: 1 }) + this.volumeSlider.value = 1; + this.volumeOnIcon.classList.remove("hidden"); + this.volumeOffIcon.classList.add("hidden"); + } + } + + updateProgress() { + const percent = (this.audio.currentTime / this.audio.duration) * 100; + this.progress.style.width = `${percent}%`; + this.currentTime.textContent = this.formatTime(this.audio.currentTime); + // Check for halfway event trigger + this.checkHalfwayEvent(percent); + } + + checkHalfwayEvent(percent) { + if (percent >= 30 && !this.hasHalfwayEventFired) { + this.hasHalfwayEventFired = true; + this.trackEvent(this.audio.dataset.trackId); // Assuming the track ID is stored in a data attribute + } + } + + trackEvent(trackId) { + fetch(`/tracks/${trackId}/events`, { + method: "GET", // Use POST to signify this is an event that is being recorded + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content + }, + // body: JSON.stringify({ event: "halfway" }) + }) + .then(response => response.json()) + .then(data => console.log("Event tracked:", data)) + .catch(error => console.error("Error tracking event:", error)); + } + + + updateDuration() { + this.duration.textContent = this.formatTime(this.audio.duration); + } + + seek(event) { + const percent = event.offsetX / event.currentTarget.offsetWidth; + this.audio.currentTime = percent * this.audio.duration; + } + + formatTime(seconds) { + const minutes = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${minutes}:${secs < 10 ? "0" : ""}${secs}`; + } + + closeSidebar(){ + this.sidebarTarget.classList.add("hidden") + } + + toggleSidebar(){ + this.sidebarTarget.classList.toggle("hidden") + } + + async nextSong() { + this.hasHalfwayEventFired = false; + const c = this.getNextTrackIndex(); + let aa = document.querySelector(`#sidebar-track-${c}`); + + if (!aa) { + const otherController = this.application.getControllerForElementAndIdentifier( + document.getElementById("track-detector"), + "track-detector" + ); + otherController.detect(); + } + + if (aa) { + const response = await get(aa.dataset.url, { + responseKind: "turbo-stream", + }); + console.log("RESPONSE", response); + console.log("Playing next song", c, aa); + } else { + console.log("No more songs to play", c, aa); + } + } + + async prevSong() { + this.hasHalfwayEventFired = false; + const c = this.getPreviousTrackIndex(); + let aa = document.querySelector(`#sidebar-track-${c}`); + + if (aa) { + const response = await get(aa.dataset.url, { + responseKind: "turbo-stream", + }); + console.log("RESPONSE", response); + console.log("Playing previous song", c, aa); + } else { + console.log("No more songs to play", c, aa); + } + } + + getNextTrackIndex() { + const { playlist } = window.store.getState(); + const currentTrackId = this.idValue + ""; + const currentTrackIndex = playlist.indexOf(currentTrackId); + + if (currentTrackIndex === -1) return playlist[0]; + return currentTrackIndex === playlist.length - 1 + ? playlist[0] + : playlist[currentTrackIndex + 1]; + } + + getPreviousTrackIndex() { + const { playlist } = window.store.getState(); + const currentTrackId = this.idValue + ""; + const currentTrackIndex = playlist.indexOf(currentTrackId); + + if (currentTrackIndex === -1) return playlist[0]; + return currentTrackIndex === 0 + ? playlist[playlist.length - 1] + : playlist[currentTrackIndex - 1]; + } +} + diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 3451e8e..56e2d46 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -43,6 +43,9 @@ import dark_mode_controller from "./dark_mode_controller.js" import simple_editor_controller from "./simple_editor_controller.jsx" import filter_manager_controller from "./filter_manager_controller.js" import accordeon_controller from "./accordeon_controller.js" +import audio_player_controller from "./audio_player_controller.js" +import marquee_controller from "./marquee_controller.js" + //import GeoChart from './geo_chart_controller' // Configure Stimulus development experience @@ -88,3 +91,5 @@ application.register("hw-combobox", HwComboboxController) application.register("input-listener", input_listener_controller) application.register("dark-mode", dark_mode_controller) application.register("accordeon", accordeon_controller) +application.register("audio-player", audio_player_controller) +application.register("marquee", marquee_controller) diff --git a/app/javascript/controllers/marquee_controller.js b/app/javascript/controllers/marquee_controller.js new file mode 100644 index 0000000..7f0f723 --- /dev/null +++ b/app/javascript/controllers/marquee_controller.js @@ -0,0 +1,26 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["marquee"]; + + connect() { + this.checkOverflow(); + } + + checkOverflow() { + const marqueeElement = this.element + const spanElement = marqueeElement.querySelector("span"); + if (true) { //(spanElement.scrollWidth > marqueeElement.clientWidth) { + // The text overflows, apply marquee animation + const overflowDistance = spanElement.scrollWidth - marqueeElement.clientWidth; + const animationDuration = overflowDistance / 100; // Adjust speed by modifying the divisor (e.g., 100) + + spanElement.style.animationDuration = `${animationDuration}s`; + marqueeElement.classList.add("marquee-active"); + } else { + // The text doesn't overflow, disable marquee + marqueeElement.classList.remove("marquee-active"); + spanElement.style.animation = "none"; // Disable animation if not overflowing + } + } +} \ No newline at end of file diff --git a/app/views/player/_player.erb b/app/views/player/_player.erb index 97d5ccc..7c72632 100644 --- a/app/views/player/_player.erb +++ b/app/views/player/_player.erb @@ -1,23 +1,26 @@ <%= turbo_frame_tag "player-frame" do %> -
- + +<% +=begin %>
- data-player-id-value="<%= track.id %>" + data-player-id-value="<%= track.id %>" data-player-peaks-value="<%= track.peaks %>" - data-player-url-value="<%= track.mp3_audio.url %>" + data-player-url-value="<%= track.mp3_audio.url %>" <% end %> class="flex"> <%= render "player/controls" %> - -
@@ -28,9 +31,150 @@ <% end %> <%= render "player/sidebar" %> - + +
+<% +=end +%> + +
+ +
+ <% if @track.cover.present? %> + <%= @track.title %> Cover Art + <% end %> + +
+
+ <%= @track.title %> +
+
+ <%= link_to @track.user.username, user_path(@track.user.username) %> +
+
+ + +
+ + + + + + + <%= link_to player_url(id: Track.where("id < ?", @track.id).order(id: :desc).first&.id ), class: "text-white" do %> + + + + <% end %> +
-<% end %> \ No newline at end of file + + +
+ +
+ + + + +
+ + + + + + <%= render "player/sidebar" %> + + +
+
+ + +
+ 0:00 +
+
+
+
+ + 4:09 + +
+ + + + + + +
+<% end %> diff --git a/app/views/player/_sidebar.erb b/app/views/player/_sidebar.erb index d0b2eac..b75cc4a 100644 --- a/app/views/player/_sidebar.erb +++ b/app/views/player/_sidebar.erb @@ -1,6 +1,6 @@