· 6 months ago · Apr 02, 2025, 07:55 AM
1-- Musiclo music player made by timuzkas
2-- licensed under Creative Commons CC0
3-- Simple, sleek player for YT.
4-- //
5-- //
6-- Backend code from @terreng on github, using MIT license
7-- Frontend code by timuzkas, using MIT license
8-- Transliterator by timuzkas, using MIT license
9-- PrimeUI by JackMacWindows, using CC0 license
10
11local expect = require "cc.expect".expect
12-- PrimeUI by JackMacWindows
13-- Public domain/CC0
14
15-- Initialization code
16local PrimeUI = {}
17do
18 local coros = {}
19 local restoreCursor
20
21 --- Adds a task to run in the main loop.
22 ---@param func function The function to run, usually an `os.pullEvent` loop
23 function PrimeUI.addTask(func)
24 expect(1, func, "function")
25 local t = {coro = coroutine.create(func)}
26 coros[#coros+1] = t
27 _, t.filter = coroutine.resume(t.coro)
28 end
29
30 --- Sends the provided arguments to the run loop, where they will be returned.
31 ---@param ... any The parameters to send
32 function PrimeUI.resolve(...)
33 coroutine.yield(coros, ...)
34 end
35
36 --- Clears the screen and resets all components. Do not use any previously
37 --- created components after calling this function.
38 function PrimeUI.clear()
39 -- Reset the screen.
40 term.setCursorPos(1, 1)
41 term.setCursorBlink(false)
42 term.setBackgroundColor(colors.black)
43 term.setTextColor(colors.white)
44 term.clear()
45 -- Reset the task list and cursor restore function.
46 coros = {}
47 restoreCursor = nil
48 end
49
50 --- Sets or clears the window that holds where the cursor should be.
51 ---@param win window|nil The window to set as the active window
52 function PrimeUI.setCursorWindow(win)
53 expect(1, win, "table", "nil")
54 restoreCursor = win and win.restoreCursor
55 end
56
57 --- Gets the absolute position of a coordinate relative to a window.
58 ---@param win window The window to check
59 ---@param x number The relative X position of the point
60 ---@param y number The relative Y position of the point
61 ---@return number x The absolute X position of the window
62 ---@return number y The absolute Y position of the window
63 function PrimeUI.getWindowPos(win, x, y)
64 if win == term then return x, y end
65 while win ~= term.native() and win ~= term.current() do
66 if not win.getPosition then return x, y end
67 local wx, wy = win.getPosition()
68 x, y = x + wx - 1, y + wy - 1
69 _, win = debug.getupvalue(select(2, debug.getupvalue(win.isColor, 1)), 1) -- gets the parent window through an upvalue
70 end
71 return x, y
72 end
73
74 --- Runs the main loop, returning information on an action.
75 ---@return any ... The result of the coroutine that exited
76 function PrimeUI.run()
77 while true do
78 -- Restore the cursor and wait for the next event.
79 if restoreCursor then restoreCursor() end
80 local ev = table.pack(os.pullEvent())
81 -- Run all coroutines.
82 for _, v in ipairs(coros) do
83 if v.filter == nil or v.filter == ev[1] then
84 -- Resume the coroutine, passing the current event.
85 local res = table.pack(coroutine.resume(v.coro, table.unpack(ev, 1, ev.n)))
86 -- If the call failed, bail out. Coroutines should never exit.
87 if not res[1] then error(res[2], 2) end
88 -- If the coroutine resolved, return its values.
89 if res[2] == coros then return table.unpack(res, 3, res.n) end
90 -- Set the next event filter.
91 v.filter = res[2]
92 end
93 end
94 end
95 end
96end
97
98--- Draws a thin border around a screen region.
99---@param win window The window to draw on
100---@param x number The X coordinate of the inside of the box
101---@param y number The Y coordinate of the inside of the box
102---@param width number The width of the inner box
103---@param height number The height of the inner box
104---@param fgColor color|nil The color of the border (defaults to white)
105---@param bgColor color|nil The color of the background (defaults to black)
106function PrimeUI.borderBox(win, x, y, width, height, fgColor, bgColor)
107 expect(1, win, "table")
108 expect(2, x, "number")
109 expect(3, y, "number")
110 expect(4, width, "number")
111 expect(5, height, "number")
112 fgColor = expect(6, fgColor, "number", "nil") or colors.white
113 bgColor = expect(7, bgColor, "number", "nil") or colors.black
114 -- Draw the top-left corner & top border.
115 win.setBackgroundColor(bgColor)
116 win.setTextColor(fgColor)
117 win.setCursorPos(x - 1, y - 1)
118 win.write("\x9C" .. ("\x8C"):rep(width))
119 -- Draw the top-right corner.
120 win.setBackgroundColor(fgColor)
121 win.setTextColor(bgColor)
122 win.write("\x93")
123 -- Draw the right border.
124 for i = 1, height do
125 win.setCursorPos(win.getCursorPos() - 1, y + i - 1)
126 win.write("\x95")
127 end
128 -- Draw the left border.
129 win.setBackgroundColor(bgColor)
130 win.setTextColor(fgColor)
131 for i = 1, height do
132 win.setCursorPos(x - 1, y + i - 1)
133 win.write("\x95")
134 end
135 -- Draw the bottom border and corners.
136 win.setCursorPos(x - 1, y + height)
137 win.write("\x8D" .. ("\x8C"):rep(width) .. "\x8E")
138end
139
140--- Creates a clickable button on screen with text.
141---@param win window The window to draw on
142---@param x number The X position of the button
143---@param y number The Y position of the button
144---@param text string The text to draw on the button
145---@param action function|string A function to call when clicked, or a string to send with a `run` event
146---@param fgColor color|nil The color of the button text (defaults to white)
147---@param bgColor color|nil The color of the button (defaults to light gray)
148---@param clickedColor color|nil The color of the button when clicked (defaults to gray)
149---@param periphName string|nil The name of the monitor peripheral, or nil (set if you're using a monitor - events will be filtered to that monitor)
150function PrimeUI.button(win, x, y, text, action, fgColor, bgColor, clickedColor, periphName)
151 expect(1, win, "table")
152 expect(1, win, "table")
153 expect(2, x, "number")
154 expect(3, y, "number")
155 expect(4, text, "string")
156 expect(5, action, "function", "string")
157 fgColor = expect(6, fgColor, "number", "nil") or colors.white
158 bgColor = expect(7, bgColor, "number", "nil") or colors.gray
159 clickedColor = expect(8, clickedColor, "number", "nil") or colors.lightGray
160 periphName = expect(9, periphName, "string", "nil")
161 -- Draw the initial button.
162 win.setCursorPos(x, y)
163 win.setBackgroundColor(bgColor)
164 win.setTextColor(fgColor)
165 win.write(" " .. text .. " ")
166 -- Get the screen position and add a click handler.
167 PrimeUI.addTask(function()
168 local buttonDown = false
169 while true do
170 local event, button, clickX, clickY = os.pullEvent()
171 local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
172 if event == "mouse_click" and periphName == nil and button == 1 and clickX >= screenX and clickX < screenX + #text + 2 and clickY == screenY then
173 -- Initiate a click action (but don't trigger until mouse up).
174 buttonDown = true
175 -- Redraw the button with the clicked background color.
176 win.setCursorPos(x, y)
177 win.setBackgroundColor(clickedColor)
178 win.setTextColor(fgColor)
179 win.write(" " .. text .. " ")
180 elseif (event == "monitor_touch" and periphName == button and clickX >= screenX and clickX < screenX + #text + 2 and clickY == screenY)
181 or (event == "mouse_up" and button == 1 and buttonDown) then
182 -- Finish a click event.
183 if clickX >= screenX and clickX < screenX + #text + 2 and clickY == screenY then
184 -- Trigger the action.
185 if type(action) == "string" then
186 PrimeUI.resolve("button", action)
187 else
188 action()
189 end
190 end
191 -- Redraw the original button state.
192 win.setCursorPos(x, y)
193 win.setBackgroundColor(bgColor)
194 win.setTextColor(fgColor)
195 win.write(" " .. text .. " ")
196 end
197 end
198 end)
199end
200
201--- Draws a line of text, centering it inside a box horizontally.
202---@param win window The window to draw on
203---@param x number The X position of the left side of the box
204---@param y number The Y position of the box
205---@param width number The width of the box to draw in
206---@param text string The text to draw
207---@param fgColor color|nil The color of the text (defaults to white)
208---@param bgColor color|nil The color of the background (defaults to black)
209function PrimeUI.centerLabel(win, x, y, width, text, fgColor, bgColor)
210 expect(1, win, "table")
211 expect(2, x, "number")
212 expect(3, y, "number")
213 expect(4, width, "number")
214 expect(5, text, "string")
215 fgColor = expect(6, fgColor, "number", "nil") or colors.white
216 bgColor = expect(7, bgColor, "number", "nil") or colors.black
217 assert(#text <= width, "string is too long")
218 win.setCursorPos(x + math.floor((width - #text) / 2), y)
219 win.setTextColor(fgColor)
220 win.setBackgroundColor(bgColor)
221 win.write(text)
222end
223
224--- Creates a list of entries with toggleable check boxes.
225---@param win window The window to draw on
226---@param x number The X coordinate of the inside of the box
227---@param y number The Y coordinate of the inside of the box
228---@param width number The width of the inner box
229---@param height number The height of the inner box
230---@param selections table<string,string|boolean> A list of entries to show, where the value is whether the item is pre-selected (or `"R"` for required/forced selected)
231---@param action function|string|nil A function or `run` event that's called when a selection is made
232---@param fgColor color|nil The color of the text (defaults to white)
233---@param bgColor color|nil The color of the background (defaults to black)
234function PrimeUI.checkSelectionBox(win, x, y, width, height, selections, action, fgColor, bgColor)
235 expect(1, win, "table")
236 expect(2, x, "number")
237 expect(3, y, "number")
238 expect(4, width, "number")
239 expect(5, height, "number")
240 expect(6, selections, "table")
241 expect(7, action, "function", "string", "nil")
242 fgColor = expect(8, fgColor, "number", "nil") or colors.white
243 bgColor = expect(9, bgColor, "number", "nil") or colors.black
244 -- Calculate how many selections there are.
245 local nsel = 0
246 for _ in pairs(selections) do nsel = nsel + 1 end
247 -- Create the outer display box.
248 local outer = window.create(win, x, y, width, height)
249 outer.setBackgroundColor(bgColor)
250 outer.clear()
251 -- Create the inner scroll box.
252 local inner = window.create(outer, 1, 1, width - 1, nsel)
253 inner.setBackgroundColor(bgColor)
254 inner.setTextColor(fgColor)
255 inner.clear()
256 -- Draw each line in the window.
257 local lines = {}
258 local nl, selected = 1, 1
259 for k, v in pairs(selections) do
260 inner.setCursorPos(1, nl)
261 inner.write((v and (v == "R" and "[-] " or "[\xD7] ") or "[ ] ") .. k)
262 lines[nl] = {k, not not v}
263 nl = nl + 1
264 end
265 -- Draw a scroll arrow if there is scrolling.
266 if nsel > height then
267 outer.setCursorPos(width, height)
268 outer.setBackgroundColor(bgColor)
269 outer.setTextColor(fgColor)
270 outer.write("\31")
271 end
272 -- Set cursor blink status.
273 inner.setCursorPos(2, selected)
274 inner.setCursorBlink(true)
275 PrimeUI.setCursorWindow(inner)
276 -- Get screen coordinates & add run task.
277 local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
278 PrimeUI.addTask(function()
279 local scrollPos = 1
280 while true do
281 -- Wait for an event.
282 local ev = table.pack(os.pullEvent())
283 -- Look for a scroll event or a selection event.
284 local dir
285 if ev[1] == "key" then
286 if ev[2] == keys.up then dir = -1
287 elseif ev[2] == keys.down then dir = 1
288 elseif ev[2] == keys.space and selections[lines[selected][1]] ~= "R" then
289 -- (Un)select the item.
290 lines[selected][2] = not lines[selected][2]
291 inner.setCursorPos(2, selected)
292 inner.write(lines[selected][2] and "\xD7" or " ")
293 -- Call the action if passed; otherwise, set the original table.
294 if type(action) == "string" then PrimeUI.resolve("checkSelectionBox", action, lines[selected][1], lines[selected][2])
295 elseif action then action(lines[selected][1], lines[selected][2])
296 else selections[lines[selected][1]] = lines[selected][2] end
297 -- Redraw all lines in case of changes.
298 for i, v in ipairs(lines) do
299 local vv = selections[v[1]] == "R" and "R" or v[2]
300 inner.setCursorPos(2, i)
301 inner.write((vv and (vv == "R" and "-" or "\xD7") or " "))
302 end
303 inner.setCursorPos(2, selected)
304 end
305 elseif ev[1] == "mouse_scroll" and ev[3] >= screenX and ev[3] < screenX + width and ev[4] >= screenY and ev[4] < screenY + height then
306 dir = ev[2]
307 end
308 -- Scroll the screen if required.
309 if dir and (selected + dir >= 1 and selected + dir <= nsel) then
310 selected = selected + dir
311 if selected - scrollPos < 0 or selected - scrollPos >= height then
312 scrollPos = scrollPos + dir
313 inner.reposition(1, 2 - scrollPos)
314 end
315 inner.setCursorPos(2, selected)
316 end
317 -- Redraw scroll arrows and reset cursor.
318 outer.setCursorPos(width, 1)
319 outer.write(scrollPos > 1 and "\30" or " ")
320 outer.setCursorPos(width, height)
321 outer.write(scrollPos < nsel - height + 1 and "\31" or " ")
322 inner.restoreCursor()
323 end
324 end)
325end
326
327--- Creates a clickable region on screen without any content.
328---@param win window The window to draw on
329---@param x number The X position of the button
330---@param y number The Y position of the button
331---@param width number The width of the inner box
332---@param height number The height of the inner box
333---@param action function|string A function to call when clicked, or a string to send with a `run` event
334---@param periphName string|nil The name of the monitor peripheral, or nil (set if you're using a monitor - events will be filtered to that monitor)
335function PrimeUI.clickRegion(win, x, y, width, height, action, periphName)
336 expect(1, win, "table")
337 expect(2, x, "number")
338 expect(3, y, "number")
339 expect(4, width, "number")
340 expect(5, height, "number")
341 expect(6, action, "function", "string")
342 expect(7, periphName, "string", "nil")
343 PrimeUI.addTask(function()
344 -- Get the screen position and add a click handler.
345 local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
346 local buttonDown = false
347 while true do
348 local event, button, clickX, clickY = os.pullEvent()
349 if (event == "monitor_touch" and periphName == button)
350 or (event == "mouse_click" and button == 1 and periphName == nil) then
351 -- Finish a click event.
352 if clickX >= screenX and clickX < screenX + width
353 and clickY >= screenY and clickY < screenY + height then
354 -- Trigger the action.
355 if type(action) == "string" then
356 PrimeUI.resolve("clickRegion", action)
357 else
358 action()
359 end
360 end
361 end
362 end
363 end)
364end
365
366--- Draws a NFT-formatted image to the screen.
367---@param win window The window to draw on
368---@param x number The X position of the top left corner of the image
369---@param y number The Y position of the top left corner of the image
370---@param data string|table The path to the image to load, or the image data itself
371function PrimeUI.drawNFT(win, x, y, data)
372 expect(1, win, "table")
373 expect(2, x, "number")
374 expect(3, y, "number")
375 expect(4, data, "string", "table")
376 -- Load the image file if a string was passed using nft.load.
377 if type(data) == "string" then
378 data = assert(nft.load("data/example.nft"), "File is not a valid NFT file")
379 end
380 nft.draw(data, x, y , win)
381end
382
383--- Draws a block of text inside a window with word wrapping, optionally resizing the window to fit.
384---@param win window The window to draw in
385---@param text string The text to draw
386---@param resizeToFit boolean|nil Whether to resize the window to fit the text (defaults to false). This is useful for scroll boxes.
387---@param fgColor color|nil The color of the text (defaults to white)
388---@param bgColor color|nil The color of the background (defaults to black)
389---@return number lines The total number of lines drawn
390function PrimeUI.drawText(win, text, resizeToFit, fgColor, bgColor)
391 expect(1, win, "table")
392 expect(2, text, "string")
393 expect(3, resizeToFit, "boolean", "nil")
394 fgColor = expect(4, fgColor, "number", "nil") or colors.white
395 bgColor = expect(5, bgColor, "number", "nil") or colors.black
396 -- Set colors.
397 win.setBackgroundColor(bgColor)
398 win.setTextColor(fgColor)
399 -- Redirect to the window to use print on it.
400 local old = term.redirect(win)
401 -- Draw the text using print().
402 local lines = print(text)
403 -- Redirect back to the original terminal.
404 term.redirect(old)
405 -- Resize the window if desired.
406 if resizeToFit then
407 -- Get original parameters.
408 local x, y = win.getPosition()
409 local w = win.getSize()
410 -- Resize the window.
411 win.reposition(x, y, w, lines)
412 end
413 return lines
414end
415
416--- Draws a horizontal line at a position with the specified width.
417---@param win window The window to draw on
418---@param x number The X position of the left side of the line
419---@param y number The Y position of the line
420---@param width number The width/length of the line
421---@param fgColor color|nil The color of the line (defaults to white)
422---@param bgColor color|nil The color of the background (defaults to black)
423function PrimeUI.horizontalLine(win, x, y, width, fgColor, bgColor)
424 expect(1, win, "table")
425 expect(2, x, "number")
426 expect(3, y, "number")
427 expect(4, width, "number")
428 fgColor = expect(5, fgColor, "number", "nil") or colors.white
429 bgColor = expect(6, bgColor, "number", "nil") or colors.black
430 -- Use drawing characters to draw a thin line.
431 win.setCursorPos(x, y)
432 win.setTextColor(fgColor)
433 win.setBackgroundColor(bgColor)
434 win.write(("\x8C"):rep(width))
435end
436
437--- Creates a text input box.
438---@param win window The window to draw on
439---@param x number The X position of the left side of the box
440---@param y number The Y position of the box
441---@param width number The width/length of the box
442---@param action function|string A function or `run` event to call when the enter key is pressed
443---@param fgColor color|nil The color of the text (defaults to white)
444---@param bgColor color|nil The color of the background (defaults to black)
445---@param replacement string|nil A character to replace typed characters with
446---@param history string[]|nil A list of previous entries to provide
447---@param completion function|nil A function to call to provide completion
448---@param default string|nil A string to return if the box is empty
449function PrimeUI.inputBox(win, x, y, width, action, fgColor, bgColor, replacement, history, completion, default)
450 expect(1, win, "table")
451 expect(2, x, "number")
452 expect(3, y, "number")
453 expect(4, width, "number")
454 expect(5, action, "function", "string")
455 fgColor = expect(6, fgColor, "number", "nil") or colors.white
456 bgColor = expect(7, bgColor, "number", "nil") or colors.black
457 expect(8, replacement, "string", "nil")
458 expect(9, history, "table", "nil")
459 expect(10, completion, "function", "nil")
460 expect(11, default, "string", "nil")
461 -- Create a window to draw the input in.
462 local box = window.create(win, x, y, width, 1)
463 box.setTextColor(fgColor)
464 box.setBackgroundColor(bgColor)
465 box.clear()
466 -- Call read() in a new coroutine.
467 PrimeUI.addTask(function()
468 -- We need a child coroutine to be able to redirect back to the window.
469 local coro = coroutine.create(read)
470 -- Run the function for the first time, redirecting to the window.
471 local old = term.redirect(box)
472 local ok, res = coroutine.resume(coro, replacement, history, completion, default)
473 term.redirect(old)
474 -- Run the coroutine until it finishes.
475 while coroutine.status(coro) ~= "dead" do
476 -- Get the next event.
477 local ev = table.pack(os.pullEvent())
478 -- Redirect and resume.
479 old = term.redirect(box)
480 ok, res = coroutine.resume(coro, table.unpack(ev, 1, ev.n))
481 term.redirect(old)
482 -- Pass any errors along.
483 if not ok then error(res) end
484 end
485 -- Send the result to the receiver.
486 if type(action) == "string" then PrimeUI.resolve("inputBox", action, res)
487 else action(res) end
488 -- Spin forever, because tasks cannot exit.
489 while true do os.pullEvent() end
490 end)
491end
492
493--- Runs a function or action repeatedly after a specified time period until canceled.
494--- If a function is passed as an action, it may return a number to change the
495--- period, or `false` to stop it.
496---@param time number The amount of time to wait for each time, in seconds
497---@param action function|string The function to call when the timer completes, or a `run` event to send
498---@return function cancel A function to cancel the timer
499function PrimeUI.interval(time, action)
500 expect(1, time, "number")
501 expect(2, action, "function", "string")
502 -- Start the timer.
503 local timer = os.startTimer(time)
504 -- Add a task to wait for the timer.
505 PrimeUI.addTask(function()
506 while true do
507 -- Wait for a timer event.
508 local _, tm = os.pullEvent("timer")
509 if tm == timer then
510 -- Fire the timer action.
511 local res
512 if type(action) == "string" then PrimeUI.resolve("timeout", action)
513 else res = action() end
514 -- Check the return value and adjust time accordingly.
515 if type(res) == "number" then time = res end
516 -- Set a new timer if not canceled.
517 if res ~= false then timer = os.startTimer(time) end
518 end
519 end
520 end)
521 -- Return a function to cancel the timer.
522 return function() os.cancelTimer(timer) end
523end
524
525--- Adds an action to trigger when a key is pressed.
526---@param key key The key to trigger on, from `keys.*`
527---@param action function|string A function to call when clicked, or a string to use as a key for a `run` return event
528function PrimeUI.keyAction(key, action)
529 expect(1, key, "number")
530 expect(2, action, "function", "string")
531 PrimeUI.addTask(function()
532 while true do
533 local _, param1 = os.pullEvent("key") -- wait for key
534 if param1 == key then
535 if type(action) == "string" then PrimeUI.resolve("keyAction", action)
536 else action() end
537 end
538 end
539 end)
540end
541
542--- Draws a line of text at a position.
543---@param win window The window to draw on
544---@param x number The X position of the left side of the text
545---@param y number The Y position of the text
546---@param text string The text to draw
547---@param fgColor color|nil The color of the text (defaults to white)
548---@param bgColor color|nil The color of the background (defaults to black)
549function PrimeUI.label(win, x, y, text, fgColor, bgColor)
550 expect(1, win, "table")
551 expect(2, x, "number")
552 expect(3, y, "number")
553 expect(4, text, "string")
554 fgColor = expect(5, fgColor, "number", "nil") or colors.white
555 bgColor = expect(6, bgColor, "number", "nil") or colors.black
556 win.setCursorPos(x, y)
557 win.setTextColor(fgColor)
558 win.setBackgroundColor(bgColor)
559 win.write(text)
560end
561
562--- Creates a progress bar, which can be updated by calling the returned function.
563---@param win window The window to draw on
564---@param x number The X position of the left side of the bar
565---@param y number The Y position of the bar
566---@param width number The width of the bar
567---@param fgColor color|nil The color of the activated part of the bar (defaults to white)
568---@param bgColor color|nil The color of the inactive part of the bar (defaults to black)
569---@param useShade boolean|nil Whether to use shaded areas for the inactive part (defaults to false)
570---@return function redraw A function to call to update the progress of the bar, taking a number from 0.0 to 1.0
571function PrimeUI.progressBar(win, x, y, width, fgColor, bgColor, useShade)
572 expect(1, win, "table")
573 expect(2, x, "number")
574 expect(3, y, "number")
575 expect(4, width, "number")
576 fgColor = expect(5, fgColor, "number", "nil") or colors.white
577 bgColor = expect(6, bgColor, "number", "nil") or colors.black
578 expect(7, useShade, "boolean", "nil")
579 local function redraw(progress)
580 expect(1, progress, "number")
581 if progress < 0 or progress > 1 then error("bad argument #1 (value out of range)", 2) end
582 -- Draw the active part of the bar.
583 win.setCursorPos(x, y)
584 win.setBackgroundColor(bgColor)
585 win.setBackgroundColor(fgColor)
586 win.write((" "):rep(math.floor(progress * width)))
587 -- Draw the inactive part of the bar, using shade if desired.
588 win.setBackgroundColor(bgColor)
589 win.setTextColor(fgColor)
590 win.write((useShade and "\x7F" or " "):rep(width - math.floor(progress * width)))
591 end
592 redraw(0)
593 return redraw
594end
595
596--- Creates a scrollable window, which allows drawing large content in a small area.
597---@param win window The parent window of the scroll box
598---@param x number The X position of the box
599---@param y number The Y position of the box
600---@param width number The width of the box
601---@param height number The height of the outer box
602---@param innerHeight number The height of the inner scroll area
603---@param allowArrowKeys boolean|nil Whether to allow arrow keys to scroll the box (defaults to true)
604---@param showScrollIndicators boolean|nil Whether to show arrow indicators on the right side when scrolling is available, which reduces the inner width by 1 (defaults to false)
605---@param fgColor number|nil The color of scroll indicators (defaults to white)
606---@param bgColor color|nil The color of the background (defaults to black)
607---@return window inner The inner window to draw inside
608---@return fun(pos:number) scroll A function to manually set the scroll position of the window
609function PrimeUI.scrollBox(win, x, y, width, height, innerHeight, allowArrowKeys, showScrollIndicators, fgColor, bgColor)
610 expect(1, win, "table")
611 expect(2, x, "number")
612 expect(3, y, "number")
613 expect(4, width, "number")
614 expect(5, height, "number")
615 expect(6, innerHeight, "number")
616 expect(7, allowArrowKeys, "boolean", "nil")
617 expect(8, showScrollIndicators, "boolean", "nil")
618 fgColor = expect(9, fgColor, "number", "nil") or colors.white
619 bgColor = expect(10, bgColor, "number", "nil") or colors.black
620 if allowArrowKeys == nil then allowArrowKeys = true end
621 -- Create the outer container box.
622 local outer = window.create(win == term and term.current() or win, x, y, width, height)
623 outer.setBackgroundColor(bgColor)
624 outer.clear()
625 -- Create the inner scrolling box.
626 local inner = window.create(outer, 1, 1, width - (showScrollIndicators and 1 or 0), innerHeight)
627 inner.setBackgroundColor(bgColor)
628 inner.clear()
629 -- Draw scroll indicators if desired.
630 if showScrollIndicators then
631 outer.setBackgroundColor(bgColor)
632 outer.setTextColor(fgColor)
633 outer.setCursorPos(width, height)
634 outer.write(innerHeight > height and "\31" or " ")
635 end
636 -- Get the absolute position of the window.
637 x, y = PrimeUI.getWindowPos(win, x, y)
638 -- Add the scroll handler.
639 local scrollPos = 1
640
641 -- Store the original event filter function
642 local originalEventFilter = PrimeUI.eventFilter
643
644 -- Replace the event filter to adjust mouse coordinates for buttons inside the scroll box
645 PrimeUI.eventFilter = function(event, ...)
646 if event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" then
647 local _, mouseX, mouseY = ...
648
649 -- Check if the click is within the scroll box boundaries
650 if mouseX >= x and mouseX < x + width and mouseY >= y and mouseY < y + height then
651 -- Adjust the Y coordinate based on scroll position
652 local adjustedY = mouseY + scrollPos - 1
653
654 -- Call the original event filter with adjusted coordinates
655 return originalEventFilter(event, _, mouseX, adjustedY, select(4, ...))
656 end
657 end
658
659 -- For other events, use the original filter
660 return originalEventFilter(event, ...)
661 end
662
663 PrimeUI.addTask(function()
664 while true do
665 -- Wait for next event.
666 local ev = table.pack(os.pullEvent())
667 -- Update inner height in case it changed.
668 innerHeight = select(2, inner.getSize())
669 -- Check for scroll events and set direction.
670 local dir
671 if ev[1] == "key" and allowArrowKeys then
672 if ev[2] == keys.up then dir = -1
673 elseif ev[2] == keys.down then dir = 1 end
674 elseif ev[1] == "mouse_scroll" and ev[3] >= x and ev[3] < x + width and ev[4] >= y and ev[4] < y + height then
675 dir = ev[2]
676 end
677 -- If there's a scroll event, move the window vertically.
678 if dir and (scrollPos + dir >= 1 and scrollPos + dir <= innerHeight - height) then
679 scrollPos = scrollPos + dir
680 inner.reposition(1, 2 - scrollPos)
681 end
682 -- Redraw scroll indicators if desired.
683 if showScrollIndicators then
684 outer.setBackgroundColor(bgColor)
685 outer.setTextColor(fgColor)
686 outer.setCursorPos(width, 1)
687 outer.write(scrollPos > 1 and "\30" or " ")
688 outer.setCursorPos(width, height)
689 outer.write(scrollPos < innerHeight - height and "\31" or " ")
690 end
691 end
692 end)
693
694 -- Make a function to allow external scrolling.
695 local function scroll(pos)
696 expect(1, pos, "number")
697 pos = math.floor(pos)
698 expect.range(pos, 1, innerHeight - height)
699 -- Scroll the window.
700 scrollPos = pos
701 inner.reposition(1, 2 - scrollPos)
702 -- Redraw scroll indicators if desired.
703 if showScrollIndicators then
704 outer.setBackgroundColor(bgColor)
705 outer.setTextColor(fgColor)
706 outer.setCursorPos(width, 1)
707 outer.write(scrollPos > 1 and "\30" or " ")
708 outer.setCursorPos(width, height)
709 outer.write(scrollPos < innerHeight - height and "\31" or " ")
710 end
711 end
712
713 -- Add a cleanup task to restore the original event filter when the scroll box is destroyed
714 PrimeUI.addTask(function()
715 while true do
716 local event = os.pullEvent("term_resize")
717 -- Check if the outer window still exists
718 if not outer.isColor then
719 -- Restore the original event filter
720 PrimeUI.eventFilter = originalEventFilter
721 return
722 end
723 end
724 end)
725
726 return inner, scroll
727end
728
729--- Creates a list of entries that can each be selected.
730---@param win window The window to draw on
731---@param x number The X coordinate of the inside of the box
732---@param y number The Y coordinate of the inside of the box
733---@param width number The width of the inner box
734---@param height number The height of the inner box
735---@param entries string[] A list of entries to show, where the value is whether the item is pre-selected (or `"R"` for required/forced selected)
736---@param action function|string A function or `run` event that's called when a selection is made
737---@param selectChangeAction function|string|nil A function or `run` event that's called when the current selection is changed
738---@param fgColor color|nil The color of the text (defaults to white)
739---@param bgColor color|nil The color of the background (defaults to black)
740function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectChangeAction, fgColor, bgColor)
741 expect(1, win, "table")
742 expect(2, x, "number")
743 expect(3, y, "number")
744 expect(4, width, "number")
745 expect(5, height, "number")
746 expect(6, entries, "table")
747 expect(7, action, "function", "string")
748 expect(8, selectChangeAction, "function", "string", "nil")
749 fgColor = expect(9, fgColor, "number", "nil") or colors.white
750 bgColor = expect(10, bgColor, "number", "nil") or colors.black
751 -- Check that all entries are strings.
752 if #entries == 0 then error("bad argument #6 (table must not be empty)", 2) end
753 for i, v in ipairs(entries) do
754 if type(v) ~= "string" then error("bad item " .. i .. " in entries table (expected string, got " .. type(v), 2) end
755 end
756 -- Create container window.
757 local entrywin = window.create(win, x, y, width, height)
758 local selection, scroll = 1, 1
759 -- Create a function to redraw the entries on screen.
760 local function drawEntries()
761 -- Clear and set invisible for performance.
762 entrywin.setVisible(false)
763 entrywin.setBackgroundColor(bgColor)
764 entrywin.clear()
765 -- Draw each entry in the scrolled region.
766 for i = scroll, scroll + height - 1 do
767 -- Get the entry; stop if there's no more.
768 local e = entries[i]
769 if not e then break end
770 -- Set the colors: invert if selected.
771 entrywin.setCursorPos(2, i - scroll + 1)
772 if i == selection then
773 entrywin.setBackgroundColor(fgColor)
774 entrywin.setTextColor(bgColor)
775 else
776 entrywin.setBackgroundColor(bgColor)
777 entrywin.setTextColor(fgColor)
778 end
779 -- Draw the selection.
780 entrywin.clearLine()
781 entrywin.write(#e > width - 1 and e:sub(1, width - 4) .. "..." or e)
782 end
783 -- Draw scroll arrows.
784 entrywin.setBackgroundColor(bgColor)
785 entrywin.setTextColor(fgColor)
786 entrywin.setCursorPos(width, 1)
787 entrywin.write("\30")
788 entrywin.setCursorPos(width, height)
789 entrywin.write("\31")
790 -- Send updates to the screen.
791 entrywin.setVisible(true)
792 end
793 -- Draw first screen.
794 drawEntries()
795 -- Add a task for selection keys.
796 PrimeUI.addTask(function()
797 while true do
798 local event, key, cx, cy = os.pullEvent()
799 if event == "key" then
800 if key == keys.down and selection < #entries then
801 -- Move selection down.
802 selection = selection + 1
803 if selection > scroll + height - 1 then scroll = scroll + 1 end
804 -- Send action if necessary.
805 if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
806 elseif selectChangeAction then selectChangeAction(selection) end
807 -- Redraw screen.
808 drawEntries()
809 elseif key == keys.up and selection > 1 then
810 -- Move selection up.
811 selection = selection - 1
812 if selection < scroll then scroll = scroll - 1 end
813 -- Send action if necessary.
814 if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
815 elseif selectChangeAction then selectChangeAction(selection) end
816 -- Redraw screen.
817 drawEntries()
818 elseif key == keys.enter then
819 -- Select the entry: send the action.
820 if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries[selection])
821 else action(entries[selection]) end
822 end
823 elseif event == "mouse_click" and key == 1 then
824 -- Handle clicking the scroll arrows.
825 local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
826 if cx == wx + width - 1 then
827 if cy == wy and selection > 1 then
828 -- Move selection up.
829 selection = selection - 1
830 if selection < scroll then scroll = scroll - 1 end
831 -- Send action if necessary.
832 if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
833 elseif selectChangeAction then selectChangeAction(selection) end
834 -- Redraw screen.
835 drawEntries()
836 elseif cy == wy + height - 1 and selection < #entries then
837 -- Move selection down.
838 selection = selection + 1
839 if selection > scroll + height - 1 then scroll = scroll + 1 end
840 -- Send action if necessary.
841 if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
842 elseif selectChangeAction then selectChangeAction(selection) end
843 -- Redraw screen.
844 drawEntries()
845 end
846 elseif cx >= wx and cx < wx + width - 1 and cy >= wy and cy < wy + height then
847 local sel = scroll + (cy - wy)
848 if sel == selection then
849 -- Select the entry: send the action.
850 if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries[selection])
851 else action(entries[selection]) end
852 else
853 selection = sel
854 -- Send action if necessary.
855 if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
856 elseif selectChangeAction then selectChangeAction(selection) end
857 -- Redraw screen.
858 drawEntries()
859 end
860 end
861 elseif event == "mouse_scroll" then
862 -- Handle mouse scrolling.
863 local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
864 if cx >= wx and cx < wx + width and cy >= wy and cy < wy + height then
865 if key < 0 and selection > 1 then
866 -- Move selection up.
867 selection = selection - 1
868 if selection < scroll then scroll = scroll - 1 end
869 -- Send action if necessary.
870 if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
871 elseif selectChangeAction then selectChangeAction(selection) end
872 -- Redraw screen.
873 drawEntries()
874 elseif key > 0 and selection < #entries then
875 -- Move selection down.
876 selection = selection + 1
877 if selection > scroll + height - 1 then scroll = scroll + 1 end
878 -- Send action if necessary.
879 if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
880 elseif selectChangeAction then selectChangeAction(selection) end
881 -- Redraw screen.
882 drawEntries()
883 end
884 end
885 end
886 end
887 end)
888end
889
890--- Creates a text box that wraps text and can have its text modified later.
891---@param win window The parent window of the text box
892---@param x number The X position of the box
893---@param y number The Y position of the box
894---@param width number The width of the box
895---@param height number The height of the box
896---@param text string The initial text to draw
897---@param fgColor color|nil The color of the text (defaults to white)
898---@param bgColor color|nil The color of the background (defaults to black)
899---@return function redraw A function to redraw the window with new contents
900function PrimeUI.textBox(win, x, y, width, height, text, fgColor, bgColor)
901 expect(1, win, "table")
902 expect(2, x, "number")
903 expect(3, y, "number")
904 expect(4, width, "number")
905 expect(5, height, "number")
906 expect(6, text, "string")
907 fgColor = expect(7, fgColor, "number", "nil") or colors.white
908 bgColor = expect(8, bgColor, "number", "nil") or colors.black
909 -- Create the box window.
910 local box = window.create(win, x, y, width, height)
911 -- Override box.getSize to make print not scroll.
912 function box.getSize()
913 return width, math.huge
914 end
915 -- Define a function to redraw with.
916 local function redraw(_text)
917 expect(1, _text, "string")
918 -- Set window parameters.
919 box.setBackgroundColor(bgColor)
920 box.setTextColor(fgColor)
921 box.clear()
922 box.setCursorPos(1, 1)
923 -- Redirect and draw with `print`.
924 local old = term.redirect(box)
925 print(_text)
926 term.redirect(old)
927 end
928 redraw(text)
929 return redraw
930end
931
932--- Creates a clickable, toggleable button on screen with text.
933---@param win window The window to draw on
934---@param x number The X position of the button
935---@param y number The Y position of the button
936---@param textOn string The text to draw on the button when on
937---@param textOff string The text to draw on the button when off (must be the same length as textOn)
938---@param action function|string A function to call when clicked, or a string to send with a `run` event
939---@param fgColor color|nil The color of the button text (defaults to white)
940---@param bgColor color|nil The color of the button (defaults to light gray)
941---@param clickedColor color|nil The color of the button when clicked (defaults to gray)
942---@param periphName string|nil The name of the monitor peripheral, or nil (set if you're using a monitor - events will be filtered to that monitor)
943function PrimeUI.toggleButton(win, x, y, textOn, textOff, action, fgColor, bgColor, clickedColor, periphName)
944 expect(1, win, "table")
945 expect(1, win, "table")
946 expect(2, x, "number")
947 expect(3, y, "number")
948 expect(4, textOn, "string")
949 expect(5, textOff, "string")
950 if #textOn ~= #textOff then error("On and off text must be the same length", 2) end
951 expect(6, action, "function", "string")
952 fgColor = expect(7, fgColor, "number", "nil") or colors.white
953 bgColor = expect(8, bgColor, "number", "nil") or colors.gray
954 clickedColor = expect(9, clickedColor, "number", "nil") or colors.lightGray
955 periphName = expect(10, periphName, "string", "nil")
956 -- Draw the initial button.
957 win.setCursorPos(x, y)
958 win.setBackgroundColor(bgColor)
959 win.setTextColor(fgColor)
960 win.write(" " .. textOff .. " ")
961 local state = false
962 -- Get the screen position and add a click handler.
963 PrimeUI.addTask(function()
964 local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
965 local buttonDown = false
966 while true do
967 local event, button, clickX, clickY = os.pullEvent()
968 if event == "mouse_click" and periphName == nil and button == 1 and clickX >= screenX and clickX < screenX + #textOn + 2 and clickY == screenY then
969 -- Initiate a click action (but don't trigger until mouse up).
970 buttonDown = true
971 -- Redraw the button with the clicked background color.
972 win.setCursorPos(x, y)
973 win.setBackgroundColor(clickedColor)
974 win.setTextColor(fgColor)
975 win.write(" " .. (state and textOn or textOff) .. " ")
976 elseif (event == "monitor_touch" and periphName == button and clickX >= screenX and clickX < screenX + #textOn + 2 and clickY == screenY)
977 or (event == "mouse_up" and button == 1 and buttonDown) then
978 -- Finish a click event.
979 state = not state
980 if clickX >= screenX and clickX < screenX + #textOn + 2 and clickY == screenY then
981 -- Trigger the action.
982 if type(action) == "string" then
983 PrimeUI.resolve("toggleButton", action, state)
984 else
985 action(state)
986 end
987 end
988 -- Redraw the original button state.
989 win.setCursorPos(x, y)
990 win.setBackgroundColor(bgColor)
991 win.setTextColor(fgColor)
992 win.write(" " .. (state and textOn or textOff) .. " ")
993 end
994 end
995 end)
996end
997
998--- Draws a vertical line at a position with the specified height.
999---@param win window The window to draw on
1000---@param x number The X position of the line
1001---@param y number The Y position of the top of the line
1002---@param height number The height of the line
1003---@param right boolean|nil Whether to align the line to the right instead of the left (defaults to false)
1004---@param fgColor color|nil The color of the line (defaults to white)
1005---@param bgColor color|nil The color of the background (defaults to black)
1006function PrimeUI.verticalLine(win, x, y, height, right, fgColor, bgColor)
1007 expect(1, win, "table")
1008 expect(2, x, "number")
1009 expect(3, y, "number")
1010 expect(4, height, "number")
1011 right = expect(5, right, "boolean", "nil") or false
1012 fgColor = expect(6, fgColor, "number", "nil") or colors.white
1013 bgColor = expect(7, bgColor, "number", "nil") or colors.black
1014 -- Use drawing characters to draw a thin line.
1015 win.setTextColor(right and bgColor or fgColor)
1016 win.setBackgroundColor(right and fgColor or bgColor)
1017 for j = 1, height do
1018 win.setCursorPos(x, y + j - 1)
1019 win.write("\x95")
1020 end
1021end
1022-- local ui = require "primeui"
1023ui = PrimeUI
1024
1025-- Transliterator | made by timuzkas
1026local Transliteration = {}
1027Transliteration.__index = Transliteration
1028
1029local cyrillicAlphabet = {
1030 {"А", "а", "A", "a"}, {"Б", "б", "B", "b"}, {"В", "в", "V", "v"}, {"Г", "г", "G", "g"}, {"Д", "д", "D", "d"},
1031 {"Е", "е", "E", "e"}, {"Ё", "ё", "YO", "yo"}, {"Ж", "ж", "ZH", "zh"}, {"З", "з", "Z", "z"}, {"И", "и", "I", "i"},
1032 {"Й", "й", "Y", "y"}, {"К", "к", "K", "k"}, {"Л", "л", "L", "l"}, {"М", "м", "M", "m"}, {"Н", "н", "N", "n"},
1033 {"О", "о", "O", "o"}, {"П", "п", "P", "p"}, {"Р", "р", "R", "r"}, {"С", "с", "S", "s"}, {"Т", "т", "T", "t"},
1034 {"У", "у", "U", "u"}, {"Ф", "ф", "F", "f"}, {"Х", "х", "KH", "kh"}, {"Ц", "ц", "TS", "ts"}, {"Ч", "ч", "CH", "ch"},
1035 {"Ш", "ш", "SH", "sh"}, {"Щ", "щ", "SHCH", "shch"}, {"Ъ", "ъ", "", ""}, {"Ы", "ы", "Y", "y"}, {"Ь", "ь", "", ""},
1036 {"Э", "э", "E", "e"}, {"Ю", "ю", "YU", "yu"}, {"Я", "я", "YA", "ya"}
1037}
1038
1039function Transliteration.new()
1040 local self = setmetatable({}, Transliteration)
1041 self.cyrillicToLatin = {}
1042 self.latinToCyrillic = {}
1043 self.isSetup = false
1044 return self
1045end
1046
1047function Transliteration:setup()
1048 if self.isSetup then return end
1049 for _, pair in ipairs(cyrillicAlphabet) do
1050 self.cyrillicToLatin[utf8.codepoint(pair[1])] = pair[3]
1051 self.cyrillicToLatin[utf8.codepoint(pair[2])] = pair[4]
1052 if pair[3] ~= "" then
1053 if not self.latinToCyrillic[pair[3]] then
1054 self.latinToCyrillic[pair[3]] = {}
1055 end
1056 table.insert(self.latinToCyrillic[pair[3]], pair[1])
1057 table.insert(self.latinToCyrillic[pair[3]], pair[2])
1058 end
1059 if pair[4] ~= "" then
1060 if not self.latinToCyrillic[pair[4]] then
1061 self.latinToCyrillic[pair[4]] = {}
1062 end
1063 table.insert(self.latinToCyrillic[pair[4]], pair[1])
1064 table.insert(self.latinToCyrillic[pair[4]], pair[2])
1065 end
1066 end
1067 self.isSetup = true
1068end
1069
1070function Transliteration:translate(str)
1071 if not self.isSetup then self:setup() end
1072 local result = ""
1073
1074 local chars = {}
1075 for char in str:gmatch(utf8.charpattern) do
1076 table.insert(chars, char)
1077 end
1078
1079 for _, char in ipairs(chars) do
1080 local codepoint = utf8.codepoint(char)
1081 local latin = self.cyrillicToLatin[codepoint]
1082 if latin then
1083 result = result .. latin
1084 else
1085 result = result .. char
1086 end
1087 end
1088
1089 return result
1090end
1091
1092-- stripped for size reasons
1093
1094local function box(terminal, x, y, width, height, color, cornerStyle)
1095 cornerStyle = cornerStyle or "square"
1096 terminal.setBackgroundColor(color)
1097
1098 if cornerStyle == "square" then
1099 for i = y, y + height - 1 do
1100 terminal.setCursorPos(x, i)
1101 terminal.write(string.rep(" ", width))
1102 end
1103 elseif cornerStyle == "round" then
1104 terminal.setCursorPos(x + 1, y)
1105 terminal.write(string.rep(" ", width - 2))
1106
1107 for i = y + 1, y + height - 2 do
1108 terminal.setCursorPos(x, i)
1109 terminal.write(string.rep(" ", width))
1110 end
1111
1112 terminal.setCursorPos(x + 1, y + height - 1)
1113 terminal.write(string.rep(" ", width - 2))
1114 end
1115end
1116ui.box = box
1117
1118local api_base_url = "https://ipod-2to6magyna-uc.a.run.app/"
1119
1120local width, height = term.getSize()
1121
1122
1123local last_search_url = nil
1124local search_results = nil
1125local playing = false
1126local queue = {}
1127local now_playing = nil
1128local looping = false
1129
1130local playing_id = nil
1131local last_download_url = nil
1132local playing_status = 0
1133
1134local player_handle = nil
1135local start = nil
1136local pcm = nil
1137local size = nil
1138local decoder = nil
1139local needs_next_chunk = 0
1140local buffer
1141
1142local speakers = { peripheral.find("speaker") }
1143
1144if #speakers == 0 then
1145 error("No speakers attached. You need to connect a speaker to this computer. If this is an Advanced Noisy Pocket Computer, then this is a bug, and you should try restarting your Minecraft game.", 0)
1146end
1147
1148local speaker = speakers[1]
1149
1150os.startTimer(1)
1151
1152-- ui helper functions
1153local function playSong(song)
1154 now_playing = song
1155 playing = true
1156 playing_id = nil
1157end
1158
1159local function stopPlayback()
1160 playing = false
1161 speaker.stop()
1162 playing_id = nil
1163end
1164
1165local function togglePlayPause()
1166 if playing then
1167 stopPlayback()
1168 else
1169 if now_playing or #queue > 0 then
1170 playSong(now_playing or queue[1])
1171 end
1172 end
1173end
1174
1175local function skipSong()
1176 if #queue > 0 then
1177 now_playing = queue[1]
1178 table.remove(queue, 1)
1179 playing_id = nil
1180 else
1181 now_playing = nil
1182 playing = false
1183 end
1184end
1185
1186local function toggleLoop()
1187 looping = not looping
1188end
1189
1190local function addToQueue(song, position)
1191 if position then
1192 table.insert(queue, position, song)
1193 else
1194 table.insert(queue, song)
1195 end
1196end
1197
1198local function removeFromQueue(position)
1199 if position and position <= #queue then
1200 table.remove(queue, position)
1201 end
1202end
1203
1204local function clearQueue()
1205 queue = {}
1206end
1207
1208local function searchMusic(query)
1209 last_search = query
1210 last_search_url = api_base_url .. "?search=" .. textutils.urlEncode(query)
1211 http.request(last_search_url)
1212 search_results = nil
1213 search_error = false
1214end
1215
1216-- not used, tho may need later
1217local function handleAudioStream(response)
1218 player_handle = response
1219 start = response.read(4)
1220 size = 16 * 1024 - 4
1221 playing_status = 1
1222 decoder = require "cc.audio.dfpwm".make_decoder()
1223end
1224
1225-- custom pallete based on spotify one.
1226local original_palette = {}
1227local function initCustomPallete()
1228 for i=1, 16 do
1229 original_palette[i] = term.getPaletteColor(i)
1230 end
1231
1232 term.setPaletteColor(colors.green, 0x1ED760)
1233 term.setPaletteColor(colors.lightGray, 0xb3b3b3)
1234 term.setPaletteColor(colors.gray, 0x212121)
1235 term.setPaletteColor(colors.purple, 0x457e59)
1236 term.setPaletteColor(colors.magenta, 0x62d089)
1237 term.setPaletteColor(colors.brown, 0x2e2e2e)
1238end
1239
1240-- truncation and transliteration for text
1241local function fixString(str, limit)
1242 if not str then return "" end
1243 --local transliterator = Transliteration.new()
1244 --str = transliterator.translate(transliterator, str)
1245
1246 if #str <= limit then
1247 return str
1248 end
1249
1250 return string.sub(str, 1, limit - 3) .. "..."
1251end
1252
1253
1254-- UI LOOP
1255
1256ui.page = 1
1257
1258local function redrawScreen()
1259 -- init custom palette
1260 initCustomPallete()
1261
1262
1263 while true do
1264 ui.clear()
1265 ui.borderBox(term.current(), 3, 2, width-4, 1, colors.gray)
1266
1267 local isSmallScreen = width <= 30
1268
1269
1270 if now_playing then
1271 if playing then
1272 if isSmallScreen then
1273 ui.button(term.current(), 4, 2, "S", "stop", colors.white, colors.red, colors.orange)
1274 else
1275 ui.button(term.current(), 4, 2, "Stop", "stop", colors.white, colors.red, colors.orange)
1276 end
1277 else
1278 ui.button(term.current(), 4, 2, "\16", "pause", colors.white, colors.green, colors.lightGray)
1279 ui.button(term.current(), 8, 2, "R", "clear", colors.white, colors.red, colors.orange)
1280 end
1281 if not isSmallScreen then
1282 ui.label(term.current(), 12, 2, fixString(now_playing.name, 20), colors.white)
1283 ui.label(term.current(), 12+string.len(fixString(now_playing.name, 20))+1, 2, "| "..fixString(now_playing.artist, 14), colors.lightGray)
1284 else
1285 ui.label(term.current(), 8, 2, fixString(now_playing.name, 16), colors.white)
1286 end
1287 else
1288 ui.label(term.current(), 4, 2, "Musiclo", colors.green)
1289 if not isSmallScreen then
1290 ui.label(term.current(), 4+string.len("Musiclo")+2, 2, "| CC:T music player made easy", colors.lightGray)
1291 else
1292 ui.label(term.current(), 4+string.len("Musiclo")+1, 2, "| CC:T player", colors.lightGray)
1293 end
1294 end
1295
1296
1297 local titleTruncateLimit = 41
1298 local artistTruncateLimit = 26
1299
1300 if isSmallScreen then
1301 titleTruncateLimit = 19
1302 artistTruncateLimit = 15
1303 end
1304
1305
1306
1307 if ui.page == 1 then
1308 ui.borderBox(term.current(), 3, 5, width-4, height-6, colors.gray)
1309
1310 ui.button(term.current(), width-9, 4, "Search", "page.2", colors.white, colors.magenta, colors.purple)
1311 ui.keyAction(keys.enter, "page.2")
1312
1313 ui.label(term.current(), 4, 4, "Queue", colors.white)
1314
1315 ui.keyAction(keys.space, "pause")
1316
1317 if looping then
1318 ui.button(term.current(), 4, height-1, "Loop", "loop", colors.white, colors.magenta, colors.purple)
1319 else
1320 ui.button(term.current(), 4, height-1, "Loop", "loop", colors.white, colors.gray, colors.lightGray)
1321 end
1322
1323 if #queue > 0 then
1324 ui.button(term.current(), 11, height-1, "Skip", "skip", colors.white, colors.gray, colors.lightGray)
1325 if isSmallScreen then
1326 ui.button(term.current(), 18, height-1, "Clr", "clear.q", colors.white, colors.red, colors.orange)
1327 else
1328 ui.button(term.current(), 18, height-1, "Clear queue", "clear.q", colors.white, colors.red, colors.orange)
1329 end
1330 end
1331 ui.label(term.current(), 4, 6, "Now playing", colors.white)
1332
1333 local scroller = ui.scrollBox(term.current(), 3, 5, width-4, height-6, 9000, true, true)
1334
1335 y = 2
1336 if #queue > 0 then
1337 for i, song in ipairs(queue) do
1338 ui.box(scroller, 1, y, width-5, 5, colors.brown)
1339 ui.label(scroller, 2, y+1, fixString(song.name, titleTruncateLimit), colors.white, colors.brown)
1340 ui.label(scroller, 2, y+2, fixString(song.artist, artistTruncateLimit), colors.lightGray, colors.brown)
1341 if isSmallScreen then y = y + 1 end
1342 ui.button(scroller, width-20, y+2, "Play", "play."..i, colors.white, colors.magenta, colors.purple)
1343 local songInQueue = false
1344 for _, queuedSong in ipairs(queue) do
1345 if queuedSong.id == song.id then
1346 songInQueue = true
1347 break
1348 end
1349 end
1350 if songInQueue then
1351 ui.button(scroller, width-13, y+2, "Remove", "rem."..i, colors.white, colors.red, colors.orange)
1352 else
1353 ui.button(scroller, width-13, y+2, "Add", "add."..i, colors.white, colors.gray, colors.lightGray)
1354 end
1355 y = y + 6
1356 end
1357 else
1358 ui.centerLabel(scroller, 1, 5,width-4, "No songs in queue", colors.lightGray)
1359 ui.button(scroller, ((width-4-3)/2-(string.len("Add song")/2))+1, 7 ,"Add song", "page.2", colors.white, colors.gray, colors.lightGray)
1360 end
1361 elseif ui.page == 2 then
1362 ui.borderBox(term.current(), 3, 5, width-4, height-6, colors.gray)
1363
1364 ui.button(term.current(), width-10, 4, "Go back","page.1", colors.white, colors.gray, colors.lightGray)
1365 ui.label(term.current(), 4, 4, "Search", colors.white)
1366
1367 ui.label(term.current(), 4, 6, "Search on Youtube...", colors.lightGray)
1368
1369 ui.horizontalLine(term.current(), 3, 8, width-4, colors.gray)
1370
1371 local scroller = ui.scrollBox(term.current(), 3, 9, width-4, height-10, 9000, true, true)
1372
1373 y = 2
1374 if search_results then
1375 for i, song in ipairs(search_results) do
1376 ui.box(scroller, 1, y, width-6, 5, colors.brown)
1377 ui.label(scroller, 2, y+1, fixString(song.name, titleTruncateLimit), colors.white, colors.brown)
1378 ui.label(scroller, 2, y+2, fixString(song.artist, artistTruncateLimit), colors.lightGray, colors.brown)
1379 if isSmallScreen then y = y + 1 end
1380 ui.button(scroller, width-21, y+2, "Play", "play."..i, colors.white, colors.magenta, colors.purple)
1381 local songInQueue = false
1382 for _, queuedSong in ipairs(queue) do
1383 if queuedSong.id == song.id then
1384 songInQueue = true
1385 break
1386 end
1387 end
1388 if songInQueue then
1389 ui.button(scroller, width-14, y+2, "Remove", "rem."..i, colors.white, colors.red, colors.orange)
1390 else
1391 ui.button(scroller, width-14, y+2, "Add", "add."..i, colors.white, colors.gray, colors.lightGray)
1392 end
1393 y = y + 6
1394 end
1395 end
1396
1397 ui.inputBox(term.current(), 4, 7, width-7, "search", colors.white, colors.gray)
1398 end
1399
1400
1401 local object, callback, text = ui.run()
1402 term.clear()
1403 term.setCursorPos(1, 1)
1404
1405 -- callbacks
1406
1407 if object == "button" then
1408 if callback == "page.2" then
1409 ui.page = 2
1410 elseif callback == "page.1" then
1411 ui.page = 1
1412 elseif callback:sub(1, 4) == "play" then
1413 local index = tonumber(callback:sub(6))
1414 if index and search_results[index] then
1415 playSong(search_results[index])
1416 ui.page = 1
1417 end
1418 elseif callback:sub(1, 3) == "add" then
1419 local index = tonumber(callback:sub(5))
1420 if index and search_results[index] then
1421 addToQueue(search_results[index])
1422 end
1423 elseif callback:sub(1, 4) == "rem" then
1424 local index = tonumber(callback:sub(6))
1425 if index and search_results[index] then
1426 removeFromQueue(index)
1427 end
1428 elseif callback == "stop" then
1429 stopPlayback()
1430 elseif callback == "pause" then
1431 togglePlayPause()
1432 elseif callback == "loop" then
1433 toggleLoop()
1434 elseif callback == "skip" then
1435 skipSong()
1436 elseif callback == "clear.q" then
1437 clearQueue()
1438 elseif callback == "clear" then
1439 playing = false
1440 now_playing = nil
1441 playing_id = nil
1442 end
1443 elseif object == "keyAction" then
1444 if callback == "page.2" then
1445 ui.page = 2
1446 elseif callback == "page.1" then
1447 ui.page = 1
1448 end
1449 elseif object == "inputBox" and callback == "search" then
1450 if text ~= "" then
1451 searchMusic(text)
1452 term.clear()
1453 local sx, sy = term.getSize()
1454 term.setTextColor(colors.lightGray)
1455 term.setCursorPos(sx/2 - #"Fetching..."/2, sy/2)
1456 term.write("Fetching...")
1457 ui.searchDone = false
1458 repeat
1459 sleep(0.1)
1460 until ui.searchDone == true
1461 ui.searchDone = false
1462 end
1463 elseif object == "rerender" then
1464 print("rerender")
1465 else
1466 term.clear()
1467 term.setCursorPos(1, 1)
1468 error("["..(object or "No object").."] "..(callback or "No callback").." "..(text or "No text").." not handled! Exiting",0)
1469 end
1470 end
1471end
1472
1473local function audioLoop()
1474 while true do
1475 -- AUDIO
1476 sleep(0.1)
1477 if playing and now_playing then
1478 if playing_id ~= now_playing.id then
1479 playing_id = now_playing.id
1480 last_download_url = api_base_url .. "?v=2&id=" .. textutils.urlEncode(playing_id)
1481 playing_status = 0
1482 needs_next_chunk = 1
1483
1484 http.request({url = last_download_url, binary = true})
1485 is_loading = true
1486
1487 end
1488 if playing_status == 1 and needs_next_chunk == 3 then
1489 needs_next_chunk = 1
1490 for _, speaker in ipairs(speakers) do
1491 while not speaker.playAudio(buffer) do
1492 needs_next_chunk = 2
1493 break
1494 end
1495 end
1496 end
1497 if playing_status == 1 and needs_next_chunk == 1 then
1498
1499 while true do
1500 local chunk = player_handle.read(size)
1501 if not chunk then
1502 if looping then
1503 playing_id = nil
1504 else
1505 if #queue > 0 then
1506 now_playing = queue[1]
1507 table.remove(queue, 1)
1508 playing_id = nil
1509 else
1510 now_playing = nil
1511 playing = false
1512 playing_id = nil
1513 is_loading = false
1514 is_error = false
1515 end
1516 end
1517
1518
1519 player_handle.close()
1520 needs_next_chunk = 0
1521 break
1522 else
1523 if start then
1524 chunk, start = start .. chunk, nil
1525 size = size + 4
1526 end
1527
1528 buffer = decoder(chunk)
1529 for _, speaker in ipairs(speakers) do
1530 while not speaker.playAudio(buffer) do
1531 needs_next_chunk = 2
1532 break
1533 end
1534 end
1535 if needs_next_chunk == 2 then
1536 break
1537 end
1538 end
1539 end
1540
1541 end
1542 end
1543 end
1544end
1545
1546-- Events
1547local function eventLoop()
1548 while true do
1549 local event, param1, param2 = os.pullEvent()
1550
1551 if event == "timer" then
1552 os.startTimer(1)
1553 end
1554
1555 if event == "speaker_audio_empty" then
1556 if needs_next_chunk == 2 then
1557 needs_next_chunk = 3
1558 end
1559 end
1560
1561 if event == "http_success" then
1562 local url = param1
1563 local handle = param2
1564 if url == last_search_url then
1565 search_results = textutils.unserialiseJSON(handle.readAll())
1566 table.remove(search_results, 1)
1567 ui.searchDone = true
1568 end
1569 if url == last_download_url then
1570 player_handle = handle
1571 start = handle.read(4)
1572 size = 16 * 1024 - 4
1573 if start == "RIFF" then
1574 error("WAV not supported!")
1575 end
1576 playing_status = 1
1577 decoder = require "cc.audio.dfpwm".make_decoder()
1578 end
1579 end
1580
1581 if event == "http_failure" then
1582 local url = param1
1583
1584 if url == last_search_url then
1585 search_error = true
1586 end
1587 if url == last_download_url then
1588 if #queue > 0 then
1589 now_playing = queue[1]
1590 table.remove(queue, 1)
1591 playing_id = nil
1592 else
1593 now_playing = nil
1594 playing = false
1595 playing_id = nil
1596 end
1597 end
1598 end
1599 end
1600end
1601
1602parallel.waitForAny(audioLoop, eventLoop, redrawScreen)
1603
1604-- cleanup
1605for i=1, 16 do
1606 term.setPaletteColor(i, original_palette[i])
1607end
1608term.setCursorBlink(false)
1609term.clear()
1610term.setCursorPos(1, 1)