cc-libs/packages/trapos-music/programs/music.lua
2026-06-20 13:45:28 +02:00

288 lines
6.4 KiB
Lua

local createVersion = require('/apis/libversion');
local command = ...;
local function printUsage()
print('music usage:');
print();
print('\t\t\tmusic');
print('\t\t\tmusic version');
print('\t\t\tmusic help');
end
if command == 'version' or command == '-version' or command == '--version' then
print('v' .. createVersion().forSelf());
return;
end
if command == 'help' or command == '-help' or command == '--help' then
printUsage();
return;
end
if command ~= nil and command ~= '' then
printUsage();
return;
end
local createMusic = require('/apis/libmusic');
local music = createMusic();
local tracks = music.listTracks();
if #tracks == 0 then
print('No music found in /data/musics');
return;
end
if not peripheral.find('speaker') then
print('No speaker attached');
return;
end
local createEventLoop = require('/apis/eventloop');
local createTui = require('/apis/libtui');
local eventloop = createEventLoop();
local ui = createTui(eventloop);
local Text = ui.Text;
local Button = ui.Button;
local Box = ui.Box;
local List = ui.List;
local player = music.createPlayer(eventloop);
-- Rows of UI chrome around the track list (header, now-playing, controls,
-- footer, plus the track box border). Used to size the scroll window.
local LIST_CHROME = 6;
local selectedIndex = 1; -- list cursor
local scrollOffset = 0; -- index of the first visible row - 1
local playingIndex = nil; -- track currently loaded in the player
local autoplay = false;
local function clampSelection()
if selectedIndex < 1 then
selectedIndex = 1;
elseif selectedIndex > #tracks then
selectedIndex = #tracks;
end
end
-- Keeps the selected row inside the visible window.
local function ensureVisible()
local _, height = term.getSize();
local visible = math.max(1, height - LIST_CHROME);
if selectedIndex <= scrollOffset then
scrollOffset = selectedIndex - 1;
elseif selectedIndex > scrollOffset + visible then
scrollOffset = selectedIndex - visible;
end
end
local function playIndex(index)
if index < 1 or index > #tracks then
return;
end
playingIndex = index;
selectedIndex = index;
player.play(tracks[index].path);
ensureVisible();
ui.rerender();
end
local function nextIndex()
return (playingIndex or selectedIndex) % #tracks + 1;
end
local function previousIndex()
return ((playingIndex or selectedIndex) - 2) % #tracks + 1;
end
player.onFinish(function()
if autoplay then
playIndex(nextIndex());
else
playingIndex = nil;
end
ui.rerender();
end);
local function Header()
return Box({
direction = 'row',
bgColor = colors.gray,
children = {
Text('Music', {
flex = 1,
color = colors.white,
bgColor = colors.gray,
}),
Button('X', {
color = colors.white,
bgColor = colors.red,
onClick = function(tui)
tui.exitUI('quit');
end,
}),
},
});
end
local function TrackRow(index)
local track = tracks[index];
local isCursor = index == selectedIndex;
local isPlaying = index == playingIndex;
local label = (isPlaying and '> ' or ' ') .. track.name;
return Button(label, {
color = isCursor and colors.black or colors.white,
bgColor = isCursor and colors.white or colors.black,
onClick = function()
playIndex(index);
end,
});
end
local function TrackList()
local _, height = term.getSize();
local visible = math.max(1, height - LIST_CHROME);
local items = {};
for offset = 1, visible do
local index = scrollOffset + offset;
if index > #tracks then
break;
end
items[#items + 1] = TrackRow(index);
end
return List({
padding = 1,
children = items,
});
end
local function statusLabel()
local s = player.getStatus();
if s == 'playing' then return 'Playing'; end
if s == 'paused' then return 'Paused'; end
return 'Stopped';
end
local function NowPlaying()
local name = playingIndex and tracks[playingIndex].name or '(none)';
return Box({
direction = 'row',
children = {
Text(statusLabel() .. ': ' .. name, { flex = 1 }),
},
});
end
local function Controls()
return Box({
direction = 'row',
gap = 1,
children = {
Button('|<', {
onClick = function()
playIndex(previousIndex());
end,
}),
Button(player.getStatus() == 'playing' and 'Pause' or 'Play', {
onClick = function()
if player.getStatus() == 'stopped' then
playIndex(selectedIndex);
else
player.togglePause();
ui.rerender();
end
end,
}),
Button('Stop', {
onClick = function()
player.stop();
playingIndex = nil;
ui.rerender();
end,
}),
Button('>|', {
onClick = function()
playIndex(nextIndex());
end,
}),
Button('Autoplay: ' .. (autoplay and 'on' or 'off'), {
flex = 1,
color = autoplay and colors.black or colors.white,
bgColor = autoplay and colors.lime or colors.black,
onClick = function()
autoplay = not autoplay;
ui.rerender();
end,
}),
},
});
end
local function Footer()
return Text(
'up/down select enter play space pause s stop n/p next/prev q quit',
{ color = colors.lightGray }
);
end
local function App()
return Box({
direction = 'column',
children = {
Header(),
Box({
flex = 1,
border = true,
title = 'Tracks',
children = {
TrackList(),
},
}),
NowPlaying(),
Controls(),
Footer(),
},
});
end
eventloop.register('key', function(key)
if key == keys.up then
selectedIndex = selectedIndex - 1;
clampSelection();
ensureVisible();
ui.rerender();
elseif key == keys.down then
selectedIndex = selectedIndex + 1;
clampSelection();
ensureVisible();
ui.rerender();
elseif key == keys.enter then
playIndex(selectedIndex);
elseif key == keys.space then
if player.getStatus() == 'stopped' then
playIndex(selectedIndex);
else
player.togglePause();
ui.rerender();
end
elseif key == keys.s then
player.stop();
playingIndex = nil;
ui.rerender();
elseif key == keys.n then
playIndex(nextIndex());
elseif key == keys.p then
playIndex(previousIndex());
elseif key == keys.q then
ui.exitUI('quit');
end
end);
ui.render(App);
player.stop();