fix app layouts: remove style tags from content fragments, use component classes
This commit is contained in:
388
base-apps/com.mosis.music/music.lua
Normal file
388
base-apps/com.mosis.music/music.lua
Normal file
@@ -0,0 +1,388 @@
|
||||
-- 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
|
||||
@@ -6,20 +6,30 @@
|
||||
<link type="text/rcss" href="../../ui/layout.rcss"/>
|
||||
<script src="../../scripts/navigation.lua"></script>
|
||||
<script src="../../scripts/layout.lua"></script>
|
||||
<script src="music.lua"></script>
|
||||
<title>Music</title>
|
||||
<style>
|
||||
.music-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding-bottom: 140px;
|
||||
}
|
||||
|
||||
.mini-player {
|
||||
position: absolute;
|
||||
bottom: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background-color: #282828;
|
||||
border-top-width: 1px;
|
||||
border-top-color: #333333;
|
||||
border-top: 1px solid #333333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mini-player:hover {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.mini-player-art {
|
||||
@@ -68,6 +78,7 @@
|
||||
.mini-control-btn img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@@ -104,10 +115,12 @@
|
||||
.recent-item {
|
||||
min-width: 120px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.recent-item:hover {
|
||||
opacity: 0.9;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.recent-art {
|
||||
@@ -214,11 +227,14 @@
|
||||
}
|
||||
|
||||
.music-bottom-nav {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
height: 56px;
|
||||
background-color: #1E1E1E;
|
||||
border-top-width: 1px;
|
||||
border-top-color: #282828;
|
||||
border-top: 1px solid #282828;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@@ -243,12 +259,29 @@
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-bottom: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-item span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Progress bar for mini player */
|
||||
.progress-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background-color: #1DB954;
|
||||
}
|
||||
|
||||
.bg-gradient-1 { background-color: #667eea; }
|
||||
.bg-gradient-2 { background-color: #f093fb; }
|
||||
.bg-gradient-3 { background-color: #4facfe; }
|
||||
@@ -260,7 +293,7 @@
|
||||
.bg-solid-blue { background-color: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app-screen" onload="initLayout(document)">
|
||||
<body class="app-screen" onload="initLayout(document); initMusic(document)">
|
||||
<!-- System Status Bar -->
|
||||
<div class="system-status-bar">
|
||||
<span id="status-time" class="system-status-time">12:30</span>
|
||||
@@ -278,7 +311,7 @@
|
||||
</div>
|
||||
<span class="app-bar-title">Music</span>
|
||||
<div class="app-bar-actions">
|
||||
<div class="app-bar-action">
|
||||
<div class="app-bar-action" onclick="openSearch()">
|
||||
<img src="../../icons/search.tga"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,28 +325,28 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Access Grid -->
|
||||
<div class="quick-access">
|
||||
<div class="quick-card">
|
||||
<div class="quick-access" id="quick-access">
|
||||
<div class="quick-card" onclick="openPlaylist('liked')">
|
||||
<div class="quick-card-art bg-solid-red">L</div>
|
||||
<span class="quick-card-title">Liked Songs</span>
|
||||
</div>
|
||||
<div class="quick-card">
|
||||
<div class="quick-card" onclick="openPlaylist('daily1')">
|
||||
<div class="quick-card-art bg-gradient-1">D</div>
|
||||
<span class="quick-card-title">Daily Mix 1</span>
|
||||
</div>
|
||||
<div class="quick-card">
|
||||
<div class="quick-card" onclick="openPlaylist('release')">
|
||||
<div class="quick-card-art bg-solid-green">R</div>
|
||||
<span class="quick-card-title">Release Radar</span>
|
||||
</div>
|
||||
<div class="quick-card">
|
||||
<div class="quick-card" onclick="openPlaylist('chill')">
|
||||
<div class="quick-card-art bg-gradient-2">C</div>
|
||||
<span class="quick-card-title">Chill Vibes</span>
|
||||
</div>
|
||||
<div class="quick-card">
|
||||
<div class="quick-card" onclick="openPlaylist('workout')">
|
||||
<div class="quick-card-art bg-solid-blue">W</div>
|
||||
<span class="quick-card-title">Workout Mix</span>
|
||||
</div>
|
||||
<div class="quick-card">
|
||||
<div class="quick-card" onclick="openPlaylist('focus')">
|
||||
<div class="quick-card-art bg-gradient-3">F</div>
|
||||
<span class="quick-card-title">Focus Flow</span>
|
||||
</div>
|
||||
@@ -325,23 +358,23 @@
|
||||
<span class="section-action">SEE ALL</span>
|
||||
</div>
|
||||
|
||||
<div class="recent-row">
|
||||
<div class="recent-item">
|
||||
<div class="recent-row" id="recent-row">
|
||||
<div class="recent-item" onclick="openPlaylist('pop')">
|
||||
<div class="recent-art bg-gradient-4">P</div>
|
||||
<div class="recent-title">Pop Hits</div>
|
||||
<div class="recent-subtitle">Playlist</div>
|
||||
</div>
|
||||
<div class="recent-item">
|
||||
<div class="recent-item" onclick="openPlaylist('electronic')">
|
||||
<div class="recent-art bg-gradient-5">E</div>
|
||||
<div class="recent-title">Electronic</div>
|
||||
<div class="recent-subtitle">Playlist</div>
|
||||
</div>
|
||||
<div class="recent-item">
|
||||
<div class="recent-item" onclick="openPlaylist('jazz')">
|
||||
<div class="recent-art bg-gradient-1">J</div>
|
||||
<div class="recent-title">Jazz Classics</div>
|
||||
<div class="recent-subtitle">Playlist</div>
|
||||
</div>
|
||||
<div class="recent-item">
|
||||
<div class="recent-item" onclick="openPlaylist('rock')">
|
||||
<div class="recent-art bg-gradient-2">R</div>
|
||||
<div class="recent-title">Rock Legends</div>
|
||||
<div class="recent-subtitle">Playlist</div>
|
||||
@@ -354,7 +387,7 @@
|
||||
<span class="section-action">SEE ALL</span>
|
||||
</div>
|
||||
|
||||
<div class="playlist-item">
|
||||
<div class="playlist-item" onclick="openPlaylist('daily1')">
|
||||
<div class="playlist-art bg-gradient-3">1</div>
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-title">Daily Mix 1</div>
|
||||
@@ -362,7 +395,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-item">
|
||||
<div class="playlist-item" onclick="openPlaylist('daily2')">
|
||||
<div class="playlist-art bg-gradient-4">2</div>
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-title">Daily Mix 2</div>
|
||||
@@ -370,7 +403,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playlist-item">
|
||||
<div class="playlist-item" onclick="openPlaylist('discover')">
|
||||
<div class="playlist-art bg-gradient-5">D</div>
|
||||
<div class="playlist-info">
|
||||
<div class="playlist-title">Discover Weekly</div>
|
||||
@@ -380,17 +413,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Mini Player -->
|
||||
<div class="mini-player">
|
||||
<div class="mini-player-art">M</div>
|
||||
<div class="mini-player" onclick="openNowPlaying()">
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div class="mini-player-art" id="mini-player-art">M</div>
|
||||
<div class="mini-player-info">
|
||||
<div class="mini-player-title">Midnight City</div>
|
||||
<div class="mini-player-artist">M83</div>
|
||||
<div class="mini-player-title" id="mini-player-title">Midnight City</div>
|
||||
<div class="mini-player-artist" id="mini-player-artist">M83</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 4px;">
|
||||
<div class="mini-control-btn">
|
||||
<div class="mini-control-btn" onclick="toggleLike(); event.stopPropagation();">
|
||||
<img src="../../icons/heart.tga"/>
|
||||
</div>
|
||||
<div class="mini-control-btn">
|
||||
<div class="mini-control-btn" id="mini-play-btn" onclick="togglePlay(); event.stopPropagation();">
|
||||
<img src="../../icons/play.tga"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,11 +438,11 @@
|
||||
<img src="../../icons/home.tga"/>
|
||||
<span>Home</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<div class="nav-item" onclick="openSearch()">
|
||||
<img src="../../icons/search.tga"/>
|
||||
<span>Search</span>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<div class="nav-item" onclick="openLibrary()">
|
||||
<img src="../../icons/library.tga"/>
|
||||
<span>Library</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user