389 lines
10 KiB
Lua
389 lines
10 KiB
Lua
-- music.lua - Music player functionality
|
|
-- Handles playback, playlists, library, and now playing
|
|
|
|
local music_doc = nil
|
|
|
|
-- Player state
|
|
local player_state = {
|
|
is_playing = false,
|
|
is_shuffled = false,
|
|
repeat_mode = "off", -- off, all, one
|
|
current_time = 0,
|
|
duration = 234, -- 3:54
|
|
volume = 80
|
|
}
|
|
|
|
-- Current track
|
|
local current_track = {
|
|
id = "1",
|
|
title = "Midnight City",
|
|
artist = "M83",
|
|
album = "Hurry Up, We're Dreaming",
|
|
duration = 234,
|
|
art_color = "#667eea"
|
|
}
|
|
|
|
-- Playlists
|
|
local playlists = {
|
|
{id = "liked", name = "Liked Songs", count = 127, color = "#dc2626"},
|
|
{id = "daily1", name = "Daily Mix 1", count = 50, color = "#667eea"},
|
|
{id = "release", name = "Release Radar", count = 30, color = "#16a34a"},
|
|
{id = "chill", name = "Chill Vibes", count = 45, color = "#f093fb"},
|
|
{id = "workout", name = "Workout Mix", count = 35, color = "#2563eb"},
|
|
{id = "focus", name = "Focus Flow", count = 40, color = "#4facfe"}
|
|
}
|
|
|
|
-- Recently played
|
|
local recently_played = {
|
|
{id = "pop", name = "Pop Hits", type = "Playlist", color = "#43e97b"},
|
|
{id = "electronic", name = "Electronic", type = "Playlist", color = "#fa709a"},
|
|
{id = "jazz", name = "Jazz Classics", type = "Playlist", color = "#667eea"},
|
|
{id = "rock", name = "Rock Legends", type = "Playlist", color = "#f093fb"}
|
|
}
|
|
|
|
-- Song queue
|
|
local queue = {
|
|
{id = "1", title = "Midnight City", artist = "M83", duration = 234},
|
|
{id = "2", title = "Intro", artist = "The xx", duration = 128},
|
|
{id = "3", title = "Retrograde", artist = "James Blake", duration = 233},
|
|
{id = "4", title = "Tame Impala", artist = "The Less I Know The Better", duration = 218},
|
|
{id = "5", title = "Redbone", artist = "Childish Gambino", duration = 327}
|
|
}
|
|
|
|
local current_queue_index = 1
|
|
local timer_id = nil
|
|
|
|
-- Initialize music app
|
|
function initMusic(doc)
|
|
print("[Music] Initializing...")
|
|
music_doc = doc
|
|
updateNowPlaying()
|
|
updateMiniPlayer()
|
|
renderPlaylists()
|
|
renderRecentlyPlayed()
|
|
end
|
|
|
|
-- Format time (seconds to mm:ss)
|
|
local function formatTime(seconds)
|
|
local mins = math.floor(seconds / 60)
|
|
local secs = seconds % 60
|
|
return string.format("%d:%02d", mins, secs)
|
|
end
|
|
|
|
-- Update now playing display
|
|
function updateNowPlaying()
|
|
if not music_doc then return end
|
|
|
|
local title = music_doc:GetElementById("now-playing-title")
|
|
local artist = music_doc:GetElementById("now-playing-artist")
|
|
|
|
if title then
|
|
title.inner_rml = current_track.title
|
|
end
|
|
if artist then
|
|
artist.inner_rml = current_track.artist
|
|
end
|
|
end
|
|
|
|
-- Update mini player
|
|
function updateMiniPlayer()
|
|
if not music_doc then return end
|
|
|
|
local title = music_doc:GetElementById("mini-player-title")
|
|
local artist = music_doc:GetElementById("mini-player-artist")
|
|
local art = music_doc:GetElementById("mini-player-art")
|
|
local play_btn = music_doc:GetElementById("mini-play-btn")
|
|
|
|
if title then
|
|
title.inner_rml = current_track.title
|
|
end
|
|
if artist then
|
|
artist.inner_rml = current_track.artist
|
|
end
|
|
if art then
|
|
art.style["background-color"] = current_track.art_color
|
|
art.inner_rml = current_track.title:sub(1,1):upper()
|
|
end
|
|
if play_btn then
|
|
local icon = player_state.is_playing and "pause.tga" or "play.tga"
|
|
play_btn.inner_rml = [[<img src="../../icons/]] .. icon .. [[" style="width: 28px; height: 28px;"/>]]
|
|
end
|
|
end
|
|
|
|
-- Toggle play/pause
|
|
function togglePlay()
|
|
player_state.is_playing = not player_state.is_playing
|
|
print("[Music] " .. (player_state.is_playing and "Playing" or "Paused"))
|
|
|
|
if player_state.is_playing then
|
|
startPlaybackTimer()
|
|
else
|
|
stopPlaybackTimer()
|
|
end
|
|
|
|
updateMiniPlayer()
|
|
updatePlayButton()
|
|
end
|
|
|
|
-- Start playback timer
|
|
function startPlaybackTimer()
|
|
if setInterval then
|
|
timer_id = setInterval(function()
|
|
if player_state.is_playing then
|
|
player_state.current_time = player_state.current_time + 1
|
|
if player_state.current_time >= current_track.duration then
|
|
nextTrack()
|
|
end
|
|
updateProgress()
|
|
end
|
|
end, 1000)
|
|
end
|
|
end
|
|
|
|
-- Stop playback timer
|
|
function stopPlaybackTimer()
|
|
if timer_id and clearInterval then
|
|
clearInterval(timer_id)
|
|
timer_id = nil
|
|
end
|
|
end
|
|
|
|
-- Update play button
|
|
function updatePlayButton()
|
|
if not music_doc then return end
|
|
|
|
local btn = music_doc:GetElementById("play-btn")
|
|
if btn then
|
|
local icon = player_state.is_playing and "pause.tga" or "play.tga"
|
|
btn.inner_rml = [[<img src="../../icons/]] .. icon .. [[" style="width: 48px; height: 48px;"/>]]
|
|
end
|
|
end
|
|
|
|
-- Update progress display
|
|
function updateProgress()
|
|
if not music_doc then return end
|
|
|
|
local current = music_doc:GetElementById("current-time")
|
|
local total = music_doc:GetElementById("total-time")
|
|
local progress = music_doc:GetElementById("progress-bar")
|
|
|
|
if current then
|
|
current.inner_rml = formatTime(player_state.current_time)
|
|
end
|
|
if total then
|
|
total.inner_rml = formatTime(current_track.duration)
|
|
end
|
|
if progress then
|
|
local percent = (player_state.current_time / current_track.duration) * 100
|
|
progress.style.width = percent .. "%"
|
|
end
|
|
end
|
|
|
|
-- Next track
|
|
function nextTrack()
|
|
print("[Music] Next track")
|
|
|
|
if player_state.is_shuffled then
|
|
current_queue_index = math.random(1, #queue)
|
|
else
|
|
current_queue_index = current_queue_index + 1
|
|
if current_queue_index > #queue then
|
|
if player_state.repeat_mode == "all" then
|
|
current_queue_index = 1
|
|
else
|
|
current_queue_index = #queue
|
|
player_state.is_playing = false
|
|
stopPlaybackTimer()
|
|
end
|
|
end
|
|
end
|
|
|
|
loadTrack(queue[current_queue_index])
|
|
end
|
|
|
|
-- Previous track
|
|
function previousTrack()
|
|
print("[Music] Previous track")
|
|
|
|
if player_state.current_time > 3 then
|
|
-- Restart current track
|
|
player_state.current_time = 0
|
|
else
|
|
current_queue_index = current_queue_index - 1
|
|
if current_queue_index < 1 then
|
|
current_queue_index = 1
|
|
end
|
|
loadTrack(queue[current_queue_index])
|
|
end
|
|
|
|
updateProgress()
|
|
end
|
|
|
|
-- Load a track
|
|
function loadTrack(track)
|
|
current_track.id = track.id
|
|
current_track.title = track.title
|
|
current_track.artist = track.artist
|
|
current_track.duration = track.duration
|
|
player_state.current_time = 0
|
|
|
|
updateNowPlaying()
|
|
updateMiniPlayer()
|
|
updateProgress()
|
|
|
|
print("[Music] Now playing: " .. track.title .. " - " .. track.artist)
|
|
|
|
if showToast then
|
|
showToast("Now playing: " .. track.title)
|
|
end
|
|
end
|
|
|
|
-- Toggle shuffle
|
|
function toggleShuffle()
|
|
player_state.is_shuffled = not player_state.is_shuffled
|
|
print("[Music] Shuffle: " .. tostring(player_state.is_shuffled))
|
|
|
|
local btn = music_doc:GetElementById("shuffle-btn")
|
|
if btn then
|
|
if player_state.is_shuffled then
|
|
btn:SetClass("active", true)
|
|
else
|
|
btn:SetClass("active", false)
|
|
end
|
|
end
|
|
|
|
if showToast then
|
|
showToast(player_state.is_shuffled and "Shuffle on" or "Shuffle off")
|
|
end
|
|
end
|
|
|
|
-- Toggle repeat
|
|
function toggleRepeat()
|
|
if player_state.repeat_mode == "off" then
|
|
player_state.repeat_mode = "all"
|
|
elseif player_state.repeat_mode == "all" then
|
|
player_state.repeat_mode = "one"
|
|
else
|
|
player_state.repeat_mode = "off"
|
|
end
|
|
|
|
print("[Music] Repeat: " .. player_state.repeat_mode)
|
|
|
|
local btn = music_doc:GetElementById("repeat-btn")
|
|
if btn then
|
|
if player_state.repeat_mode ~= "off" then
|
|
btn:SetClass("active", true)
|
|
else
|
|
btn:SetClass("active", false)
|
|
end
|
|
end
|
|
|
|
if showToast then
|
|
local msg = "Repeat: " .. player_state.repeat_mode
|
|
showToast(msg)
|
|
end
|
|
end
|
|
|
|
-- Toggle like
|
|
function toggleLike()
|
|
print("[Music] Toggle like")
|
|
if showToast then
|
|
showToast("Added to Liked Songs")
|
|
end
|
|
end
|
|
|
|
-- Render playlists
|
|
function renderPlaylists()
|
|
if not music_doc then return end
|
|
|
|
local container = music_doc:GetElementById("quick-access")
|
|
if not container then return end
|
|
|
|
local html = ""
|
|
for i, pl in ipairs(playlists) do
|
|
if i <= 6 then
|
|
local initial = pl.name:sub(1,1):upper()
|
|
html = html .. [[
|
|
<div class="quick-card" onclick="openPlaylist(']] .. pl.id .. [[')">
|
|
<div class="quick-card-art" style="background-color: ]] .. pl.color .. [[;">]] .. initial .. [[</div>
|
|
<span class="quick-card-title">]] .. pl.name .. [[</span>
|
|
</div>
|
|
]]
|
|
end
|
|
end
|
|
|
|
container.inner_rml = html
|
|
end
|
|
|
|
-- Render recently played
|
|
function renderRecentlyPlayed()
|
|
if not music_doc then return end
|
|
|
|
local container = music_doc:GetElementById("recent-row")
|
|
if not container then return end
|
|
|
|
local html = ""
|
|
for _, item in ipairs(recently_played) do
|
|
local initial = item.name:sub(1,1):upper()
|
|
html = html .. [[
|
|
<div class="recent-item" onclick="openPlaylist(']] .. item.id .. [[')">
|
|
<div class="recent-art" style="background-color: ]] .. item.color .. [[;">]] .. initial .. [[</div>
|
|
<div class="recent-title">]] .. item.name .. [[</div>
|
|
<div class="recent-subtitle">]] .. item.type .. [[</div>
|
|
</div>
|
|
]]
|
|
end
|
|
|
|
container.inner_rml = html
|
|
end
|
|
|
|
-- Open playlist
|
|
function openPlaylist(playlist_id)
|
|
print("[Music] Opening playlist: " .. playlist_id)
|
|
if navigateTo then
|
|
navigateTo("playlist_" .. playlist_id)
|
|
else
|
|
if showToast then
|
|
showToast("Playlist: " .. playlist_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Open now playing
|
|
function openNowPlaying()
|
|
print("[Music] Opening now playing...")
|
|
if navigateTo then
|
|
navigateTo("now_playing")
|
|
end
|
|
end
|
|
|
|
-- Open search
|
|
function openSearch()
|
|
print("[Music] Opening search...")
|
|
if navigateTo then
|
|
navigateTo("music_search")
|
|
else
|
|
if showToast then
|
|
showToast("Search music")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Open library
|
|
function openLibrary()
|
|
print("[Music] Opening library...")
|
|
if navigateTo then
|
|
navigateTo("music_library")
|
|
else
|
|
if showToast then
|
|
showToast("Your library")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Seek to position (0-1)
|
|
function seekTo(position)
|
|
player_state.current_time = math.floor(position * current_track.duration)
|
|
updateProgress()
|
|
end
|