-- pocket_compact_controller.txt
-- Compact controller UI for Advanced Wireless Pocket Computer.
-- Requires server to include server_cmd_patch.txt and to run
-- mirrorHelloLoop + mirrorStatusLoop.
local MODE = "ui-mirror-v1"
-- Open rednet
local function _openRednetClient()
local side = nil
for _, s in ipairs(rs.getSides()) do
if peripheral.getType(s) == "modem" then side = s break end
end
assert(side, "Wireless modem required on client")
if not rednet.isOpen(side) then rednet.open(side) end
return side
end
_openRednetClient()
-- State mirrored from server
local S = {
now = nil, playing = false, looping = 0, volume = 1.0,
is_loading = false, is_error = false, queue = {}, last_search = nil,
search_results = nil,
}
local cw, ch = term.getSize()
local TAB = 1 -- 1=Now, 2=Queue, 3=Find
local selIdx = 1 -- selection index for lists
local resIdx = 1 -- selection for search results
local serverId = nil
-- Helpers
local function send(pkt)
if serverId then rednet.send(serverId, pkt, MODE) else rednet.broadcast(pkt, MODE) end
end
local function cmd(name, extra)
local m = { op="CMD", cmd=name }
if extra then for k,v in pairs(extra) do m[k]=v end end
send(m)
end
-- UI draw
local function bar(t) term.setBackgroundColor(colors.gray) term.setTextColor(colors.white) term.setCursorPos(1,1) term.clearLine() term.write(t) end
local function footer(t) term.setCursorPos(1,ch) term.setBackgroundColor(colors.gray) term.setTextColor(colors.white) term.clearLine() term.write(t) end
local function clearBody() term.setBackgroundColor(colors.black) for y=2,ch-1 do term.setCursorPos(1,y) term.clearLine() end end
local function drawNow()
bar(" Now | Queue | Find ")
clearBody()
term.setTextColor(colors.white)
term.setCursorPos(2,3)
term.write(S.now and (S.now.name or "Unknown") or "Not playing")
term.setCursorPos(2,4)
term.setTextColor(colors.lightGray)
term.write(S.now and (S.now.artist or "") or "")
term.setTextColor(colors.white)
term.setCursorPos(2,6)
local status = (S.is_error and "Error") or (S.is_loading and "Loading") or (S.playing and "Playing") or "Stopped"
term.write(("Status: %s"):format(status))
-- Volume line
local pct = math.floor((S.volume/3)*100 + 0.5)
term.setCursorPos(2,8); term.write(("Vol: %3d%%"):format(pct))
-- Loop state
local loopTxt = (S.looping==0 and "Off") or (S.looping==1 and "Queue") or "Song"
term.setCursorPos(2,9); term.write("Loop: "..loopTxt)
footer("⏯ Play/Pause ⏭ Skip L Loop - Vol+ + Vol- 2 Queue 3 Find")
end
local function drawQueue()
bar(" Now |*Queue*| Find ")
clearBody()
term.setTextColor(colors.white)
term.setCursorPos(2,2) term.write("Up Next:")
for i=1, math.min(#S.queue, ch-5) do
local y = 2 + i
term.setCursorPos(2,y)
if i==selIdx then term.setBackgroundColor(colors.white) term.setTextColor(colors.black) else term.setBackgroundColor(colors.black) term.setTextColor(colors.white) end
local name = S.queue[i].name or "Unknown"
term.write(name:sub(1, cw-2))
term.setBackgroundColor(colors.black) term.setTextColor(colors.lightGray)
term.setCursorPos(2,y+1)
if S.queue[i].artist then term.write(("by %s"):format(S.queue[i].artist):sub(1, cw-2)) end
y = y + 1
end
footer("↑/↓ Select Enter Play D Delete 1 Now 3 Find")
end
local function drawFind()
bar(" Now | Queue |*Find*")
clearBody()
term.setCursorPos(2,2) term.setTextColor(colors.white) term.write("Query:")
term.setCursorPos(2,3) term.setTextColor(colors.yellow)
term.write(S.last_search or "(press S to set)")
term.setTextColor(colors.white)
term.setCursorPos(2,5) term.write("Results:")
local list = S.search_results or {}
for i=1, math.min(#list, ch-8) do
local y = 5 + i
term.setCursorPos(2,y)
if i==resIdx then term.setBackgroundColor(colors.white) term.setTextColor(colors.black) else term.setBackgroundColor(colors.black) term.setTextColor(colors.white) end
local name = list[i].name or "Unknown"
term.write(name:sub(1, cw-2))
term.setBackgroundColor(colors.black) term.setTextColor(colors.lightGray)
term.setCursorPos(2,y+1)
if list[i].artist then term.write(("by %s"):format(list[i].artist):sub(1, cw-2)) end
end
footer("S Search Enter Play N PlayNext A Add 1 Now 2 Queue")
end
local function redraw()
term.setBackgroundColor(colors.black) term.clear() term.setCursorPos(1,1)
if TAB==1 then drawNow()
elseif TAB==2 then drawQueue()
else drawFind() end
end
-- Net receive
local function netLoop()
while true do
local id, msg, proto = rednet.receive(MODE)
if proto == MODE and type(msg) == "table" then
if msg.op == "HELLO_ACK" then
serverId = id
elseif msg.op == "STATUS" then
serverId = serverId or id
S.now = msg.now; S.playing = msg.playing; S.looping = msg.looping
S.volume = msg.volume; S.is_loading = msg.is_loading; S.is_error = msg.is_error
S.queue = msg.queue or {}; S.last_search = msg.last_search; S.search_results = msg.search_results
redraw()
elseif msg.op == "screen" then
serverId = serverId or id
end
end
end
end
-- Periodic hello + status requests
local function helloLoop()
while true do
if not serverId then rednet.broadcast({ op="HELLO", w=cw, h=ch }, MODE) end
send({ op="STATUS_REQ" })
sleep(1.0)
end
end
-- Input
local function inputLoop()
redraw()
while true do
local e, p1, p2, p3 = os.pullEvent()
if e == "key" then
local k = p1
if k == keys.one then TAB=1; redraw()
elseif k == keys.two then TAB=2; redraw()
elseif k == keys.three then TAB=3; redraw()
elseif TAB == 1 then
if k == keys.space then cmd("PLAY_TOGGLE")
elseif k == keys.l then cmd("LOOP_NEXT")
elseif k == keys.minus then cmd("VOL_SET", {value = math.max(0, (S.volume or 1)-0.15)})
elseif k == keys.equals or k == keys.plus then cmd("VOL_SET", {value = math.min(3, (S.volume or 1)+0.15)})
elseif k == keys.right then cmd("SKIP")
end
elseif TAB == 2 then
if k == keys.up then selIdx = math.max(1, selIdx-1); redraw()
elseif k == keys.down then selIdx = math.min(math.max(#S.queue,1), selIdx+1); redraw()
elseif k == keys.enter then cmd("PLAY_FROM_QUEUE_IDX", {idx = selIdx})
elseif k == keys.d then cmd("REMOVE_QUEUE_IDX", {idx = selIdx})
end
elseif TAB == 3 then
if k == keys.up then resIdx = math.max(1, resIdx-1); redraw()
elseif k == keys.down then resIdx = math.min(math.max(#(S.search_results or {}),1), resIdx+1); redraw()
elseif k == keys.s then
term.setCursorBlink(true)
term.setCursorPos(2,3)
term.setTextColor(colors.white) term.setBackgroundColor(colors.black)
term.clearLine() term.setCursorPos(2,3)
local q = read()
term.setCursorBlink(false)
if q and #q>0 then cmd("SEARCH", {q=q}) end
elseif k == keys.enter then cmd("PLAY_NOW", {idx = resIdx})
elseif k == keys.n then cmd("PLAY_NEXT", {idx = resIdx})
elseif k == keys.a then cmd("ADD_QUEUE", {idx = resIdx})
end
end
elseif e == "mouse_click" then
local x,y = p2,p3
-- Top bar tabs by rough regions
if y == 1 then
if x <= 8 then TAB=1 elseif x <= 16 then TAB=2 else TAB=3 end
redraw()
end
end
end
end
-- Start
term.setBackgroundColor(colors.black) term.clear() term.setCursorPos(1,1)
print("Pocket controller")
parallel.waitForAny(helloLoop, netLoop, inputLoop)
Comments