-- 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)