TIMAS_Bro

Untitled

Apr 3rd, 2026 (edited)
49
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 54.90 KB | None | 0 0
  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 CC0 license
  8. -- Transliterator by timuzkas, using CC0 license
  9. -- PrimeUI by JackMacWindows, using CC0 license
  10.  
  11. local expect = require "cc.expect".expect
  12. -- PrimeUI by JackMacWindows
  13. -- Public domain/CC0
  14.  
  15. -- Initialization code
  16. local PrimeUI = {}
  17. do
  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
  96. end
  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)
  106. function 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")
  138. end
  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)
  150. function 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. buttonDown = false
  197. end
  198. end
  199. end)
  200. end
  201.  
  202. --- Draws a line of text, centering it inside a box horizontally.
  203. ---@param win window The window to draw on
  204. ---@param x number The X position of the left side of the box
  205. ---@param y number The Y position of the box
  206. ---@param width number The width of the box to draw in
  207. ---@param text string The text to draw
  208. ---@param fgColor color|nil The color of the text (defaults to white)
  209. ---@param bgColor color|nil The color of the background (defaults to black)
  210. function PrimeUI.centerLabel(win, x, y, width, text, fgColor, bgColor)
  211. expect(1, win, "table")
  212. expect(2, x, "number")
  213. expect(3, y, "number")
  214. expect(4, width, "number")
  215. expect(5, text, "string")
  216. fgColor = expect(6, fgColor, "number", "nil") or colors.white
  217. bgColor = expect(7, bgColor, "number", "nil") or colors.black
  218. assert(#text <= width, "string is too long")
  219. win.setCursorPos(x + math.floor((width - #text) / 2), y)
  220. win.setTextColor(fgColor)
  221. win.setBackgroundColor(bgColor)
  222. win.write(text)
  223. end
  224.  
  225. --- Creates a clickable region on screen without any content.
  226. ---@param win window The window to draw on
  227. ---@param x number The X position of the button
  228. ---@param y number The Y position of the button
  229. ---@param width number The width of the inner box
  230. ---@param height number The height of the inner box
  231. ---@param action function|string A function to call when clicked, or a string to send with a `run` event
  232. ---@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)
  233. function PrimeUI.clickRegion(win, x, y, width, height, action, periphName)
  234. expect(1, win, "table")
  235. expect(2, x, "number")
  236. expect(3, y, "number")
  237. expect(4, width, "number")
  238. expect(5, height, "number")
  239. expect(6, action, "function", "string")
  240. expect(7, periphName, "string", "nil")
  241. PrimeUI.addTask(function()
  242. -- Get the screen position and add a click handler.
  243. local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
  244. while true do
  245. local event, button, clickX, clickY = os.pullEvent()
  246. if (event == "monitor_touch" and periphName == button)
  247. or (event == "mouse_click" and button == 1 and periphName == nil) then
  248. -- Finish a click event.
  249. if clickX >= screenX and clickX < screenX + width
  250. and clickY >= screenY and clickY < screenY + height then
  251. -- Trigger the action.
  252. if type(action) == "string" then
  253. PrimeUI.resolve("clickRegion", action)
  254. else
  255. action()
  256. end
  257. end
  258. end
  259. end
  260. end)
  261. end
  262.  
  263. --- Draws a block of text inside a window with word wrapping, optionally resizing the window to fit.
  264. ---@param win window The window to draw in
  265. ---@param text string The text to draw
  266. ---@param resizeToFit boolean|nil Whether to resize the window to fit the text (defaults to false). This is useful for scroll boxes.
  267. ---@param fgColor color|nil The color of the text (defaults to white)
  268. ---@param bgColor color|nil The color of the background (defaults to black)
  269. ---@return number lines The total number of lines drawn
  270. function PrimeUI.drawText(win, text, resizeToFit, fgColor, bgColor)
  271. expect(1, win, "table")
  272. expect(2, text, "string")
  273. expect(3, resizeToFit, "boolean", "nil")
  274. fgColor = expect(4, fgColor, "number", "nil") or colors.white
  275. bgColor = expect(5, bgColor, "number", "nil") or colors.black
  276. -- Set colors.
  277. win.setBackgroundColor(bgColor)
  278. win.setTextColor(fgColor)
  279. -- Redirect to the window to use print on it.
  280. local old = term.redirect(win)
  281. -- Draw the text using print().
  282. local lines = print(text)
  283. -- Redirect back to the original terminal.
  284. term.redirect(old)
  285. -- Resize the window if desired.
  286. if resizeToFit then
  287. -- Get original parameters.
  288. local x, y = win.getPosition()
  289. local w = win.getSize()
  290. -- Resize the window.
  291. win.reposition(x, y, w, lines)
  292. end
  293. return lines
  294. end
  295.  
  296. --- Draws a horizontal line at a position with the specified width.
  297. ---@param win window The window to draw on
  298. ---@param x number The X position of the left side of the line
  299. ---@param y number The Y position of the line
  300. ---@param width number The width/length of the line
  301. ---@param fgColor color|nil The color of the line (defaults to white)
  302. ---@param bgColor color|nil The color of the background (defaults to black)
  303. function PrimeUI.horizontalLine(win, x, y, width, fgColor, bgColor)
  304. expect(1, win, "table")
  305. expect(2, x, "number")
  306. expect(3, y, "number")
  307. expect(4, width, "number")
  308. fgColor = expect(5, fgColor, "number", "nil") or colors.white
  309. bgColor = expect(6, bgColor, "number", "nil") or colors.black
  310. -- Use drawing characters to draw a thin line.
  311. win.setCursorPos(x, y)
  312. win.setTextColor(fgColor)
  313. win.setBackgroundColor(bgColor)
  314. win.write(("\x8C"):rep(width))
  315. end
  316.  
  317. --- Creates a text input box.
  318. ---@param win window The window to draw on
  319. ---@param x number The X position of the left side of the box
  320. ---@param y number The Y position of the box
  321. ---@param width number The width/length of the box
  322. ---@param action function|string A function or `run` event to call when the enter key is pressed
  323. ---@param fgColor color|nil The color of the text (defaults to white)
  324. ---@param bgColor color|nil The color of the background (defaults to black)
  325. ---@param replacement string|nil A character to replace typed characters with
  326. ---@param history string[]|nil A list of previous entries to provide
  327. ---@param completion function|nil A function to call to provide completion
  328. ---@param default string|nil A string to return if the box is empty
  329. function PrimeUI.inputBox(win, x, y, width, action, fgColor, bgColor, replacement, history, completion, default)
  330. expect(1, win, "table")
  331. expect(2, x, "number")
  332. expect(3, y, "number")
  333. expect(4, width, "number")
  334. expect(5, action, "function", "string")
  335. fgColor = expect(6, fgColor, "number", "nil") or colors.white
  336. bgColor = expect(7, bgColor, "number", "nil") or colors.black
  337. expect(8, replacement, "string", "nil")
  338. expect(9, history, "table", "nil")
  339. expect(10, completion, "function", "nil")
  340. expect(11, default, "string", "nil")
  341. -- Create a window to draw the input in.
  342. local box = window.create(win, x, y, width, 1)
  343. box.setTextColor(fgColor)
  344. box.setBackgroundColor(bgColor)
  345. box.clear()
  346. -- Call read() in a new coroutine.
  347. PrimeUI.addTask(function()
  348. -- We need a child coroutine to be able to redirect back to the window.
  349. local coro = coroutine.create(read)
  350. -- Run the function for the first time, redirecting to the window.
  351. local old = term.redirect(box)
  352. local ok, res = coroutine.resume(coro, replacement, history, completion, default)
  353. term.redirect(old)
  354. -- Run the coroutine until it finishes.
  355. while coroutine.status(coro) ~= "dead" do
  356. -- Get the next event.
  357. local ev = table.pack(os.pullEvent())
  358. -- Redirect and resume.
  359. old = term.redirect(box)
  360. ok, res = coroutine.resume(coro, table.unpack(ev, 1, ev.n))
  361. term.redirect(old)
  362. -- Pass any errors along.
  363. if not ok then error(res) end
  364. end
  365. -- Send the result to the receiver.
  366. if type(action) == "string" then PrimeUI.resolve("inputBox", action, res)
  367. else action(res) end
  368. -- Spin forever, because tasks cannot exit.
  369. while true do os.pullEvent() end
  370. end)
  371. end
  372.  
  373. --- Runs a function or action repeatedly after a specified time period until canceled.
  374. ---@param time number The amount of time to wait for each time, in seconds
  375. ---@param action function|string The function to call when the timer completes, or a `run` event to send
  376. ---@return function cancel A function to cancel the timer
  377. function PrimeUI.interval(time, action)
  378. expect(1, time, "number")
  379. expect(2, action, "function", "string")
  380. -- Start the timer.
  381. local timer = os.startTimer(time)
  382. -- Add a task to wait for the timer.
  383. PrimeUI.addTask(function()
  384. while true do
  385. -- Wait for a timer event.
  386. local _, tm = os.pullEvent("timer")
  387. if tm == timer then
  388. -- Fire the timer action.
  389. local res
  390. if type(action) == "string" then PrimeUI.resolve("timeout", action)
  391. else res = action() end
  392. -- Check the return value and adjust time accordingly.
  393. if type(res) == "number" then time = res end
  394. -- Set a new timer if not canceled.
  395. if res ~= false then timer = os.startTimer(time) end
  396. end
  397. end
  398. end)
  399. -- Return a function to cancel the timer.
  400. return function() os.cancelTimer(timer) end
  401. end
  402.  
  403. --- Adds an action to trigger when a key is pressed.
  404. ---@param key key The key to trigger on, from `keys.*`
  405. ---@param action function|string A function to call when clicked, or a string to use as a key for a `run` return event
  406. function PrimeUI.keyAction(key, action)
  407. expect(1, key, "number")
  408. expect(2, action, "function", "string")
  409. PrimeUI.addTask(function()
  410. while true do
  411. local _, param1 = os.pullEvent("key") -- wait for key
  412. if param1 == key then
  413. if type(action) == "string" then PrimeUI.resolve("keyAction", action)
  414. else action() end
  415. end
  416. end
  417. end)
  418. end
  419.  
  420. --- Draws a line of text at a position.
  421. ---@param win window The window to draw on
  422. ---@param x number The X position of the left side of the text
  423. ---@param y number The Y position of the text
  424. ---@param text string The text to draw
  425. ---@param fgColor color|nil The color of the text (defaults to white)
  426. ---@param bgColor color|nil The color of the background (defaults to black)
  427. function PrimeUI.label(win, x, y, text, fgColor, bgColor)
  428. expect(1, win, "table")
  429. expect(2, x, "number")
  430. expect(3, y, "number")
  431. expect(4, text, "string")
  432. fgColor = expect(5, fgColor, "number", "nil") or colors.white
  433. bgColor = expect(6, bgColor, "number", "nil") or colors.black
  434. win.setCursorPos(x, y)
  435. win.setTextColor(fgColor)
  436. win.setBackgroundColor(bgColor)
  437. win.write(text)
  438. end
  439.  
  440. --- Creates a progress bar, which can be updated by calling the returned function.
  441. ---@param win window The window to draw on
  442. ---@param x number The X position of the left side of the bar
  443. ---@param y number The Y position of the bar
  444. ---@param width number The width of the bar
  445. ---@param fgColor color|nil The color of the activated part of the bar (defaults to white)
  446. ---@param bgColor color|nil The color of the inactive part of the bar (defaults to black)
  447. ---@param useShade boolean|nil Whether to use shaded areas for the inactive part (defaults to false)
  448. ---@return function redraw A function to call to update the progress of the bar, taking a number from 0.0 to 1.0
  449. function PrimeUI.progressBar(win, x, y, width, fgColor, bgColor, useShade)
  450. expect(1, win, "table")
  451. expect(2, x, "number")
  452. expect(3, y, "number")
  453. expect(4, width, "number")
  454. fgColor = expect(5, fgColor, "number", "nil") or colors.white
  455. bgColor = expect(6, bgColor, "number", "nil") or colors.black
  456. expect(7, useShade, "boolean", "nil")
  457. local function redraw(progress)
  458. expect(1, progress, "number")
  459. if progress < 0 or progress > 1 then error("bad argument #1 (value out of range)", 2) end
  460. win.setCursorPos(x, y)
  461. win.setBackgroundColor(fgColor)
  462. win.write((" "):rep(math.floor(progress * width)))
  463. win.setBackgroundColor(bgColor)
  464. win.setTextColor(fgColor)
  465. win.write((useShade and "\x7F" or " "):rep(width - math.floor(progress * width)))
  466. end
  467. redraw(0)
  468. return redraw
  469. end
  470.  
  471. --- Creates a scrollable window, which allows drawing large content in a small area.
  472. ---@param win window The parent window of the scroll box
  473. ---@param x number The X position of the box
  474. ---@param y number The Y position of the box
  475. ---@param width number The width of the box
  476. ---@param height number The height of the outer box
  477. ---@param innerHeight number The height of the inner scroll area
  478. ---@param allowArrowKeys boolean|nil Whether to allow arrow keys to scroll the box (defaults to true)
  479. ---@param showScrollIndicators boolean|nil Whether to show arrow indicators on the right side when scrolling is available (defaults to false)
  480. ---@param fgColor number|nil The color of scroll indicators (defaults to white)
  481. ---@param bgColor color|nil The color of the background (defaults to black)
  482. ---@return window inner The inner window to draw inside
  483. ---@return fun(pos:number) scroll A function to manually set the scroll position of the window
  484. function PrimeUI.scrollBox(win, x, y, width, height, innerHeight, allowArrowKeys, showScrollIndicators, fgColor, bgColor)
  485. expect(1, win, "table")
  486. expect(2, x, "number")
  487. expect(3, y, "number")
  488. expect(4, width, "number")
  489. expect(5, height, "number")
  490. expect(6, innerHeight, "number")
  491. expect(7, allowArrowKeys, "boolean", "nil")
  492. expect(8, showScrollIndicators, "boolean", "nil")
  493. fgColor = expect(9, fgColor, "number", "nil") or colors.white
  494. bgColor = expect(10, bgColor, "number", "nil") or colors.black
  495. if allowArrowKeys == nil then allowArrowKeys = true end
  496. -- Create the outer container box.
  497. local outer = window.create(win == term and term.current() or win, x, y, width, height)
  498. outer.setBackgroundColor(bgColor)
  499. outer.clear()
  500. -- Create the inner scrolling box.
  501. local inner = window.create(outer, 1, 1, width - (showScrollIndicators and 1 or 0), innerHeight)
  502. inner.setBackgroundColor(bgColor)
  503. inner.clear()
  504. -- Draw scroll indicators if desired.
  505. if showScrollIndicators then
  506. outer.setBackgroundColor(bgColor)
  507. outer.setTextColor(fgColor)
  508. outer.setCursorPos(width, height)
  509. outer.write(innerHeight > height and "\31" or " ")
  510. end
  511. -- Get the absolute position of the window.
  512. x, y = PrimeUI.getWindowPos(win, x, y)
  513. -- Add the scroll handler.
  514. local scrollPos = 1
  515. PrimeUI.addTask(function()
  516. while true do
  517. -- Wait for next event.
  518. local ev = table.pack(os.pullEvent())
  519. -- Update inner height in case it changed.
  520. innerHeight = select(2, inner.getSize())
  521. -- Check for scroll events and set direction.
  522. local dir
  523. if ev[1] == "key" and allowArrowKeys then
  524. if ev[2] == keys.up then dir = -1
  525. elseif ev[2] == keys.down then dir = 1 end
  526. elseif ev[1] == "mouse_scroll" and ev[3] >= x and ev[3] < x + width and ev[4] >= y and ev[4] < y + height then
  527. dir = ev[2]
  528. end
  529. -- If there's a scroll event, move the window vertically.
  530. if dir and (scrollPos + dir >= 1 and scrollPos + dir <= innerHeight - height) then
  531. scrollPos = scrollPos + dir
  532. inner.reposition(1, 2 - scrollPos)
  533. end
  534. -- Redraw scroll indicators if desired.
  535. if showScrollIndicators then
  536. outer.setBackgroundColor(bgColor)
  537. outer.setTextColor(fgColor)
  538. outer.setCursorPos(width, 1)
  539. outer.write(scrollPos > 1 and "\30" or " ")
  540. outer.setCursorPos(width, height)
  541. outer.write(scrollPos < innerHeight - height and "\31" or " ")
  542. end
  543. end
  544. end)
  545.  
  546. -- Make a function to allow external scrolling.
  547. local function scroll(pos)
  548. expect(1, pos, "number")
  549. pos = math.floor(pos)
  550. expect.range(pos, 1, innerHeight - height)
  551. scrollPos = pos
  552. inner.reposition(1, 2 - scrollPos)
  553. if showScrollIndicators then
  554. outer.setBackgroundColor(bgColor)
  555. outer.setTextColor(fgColor)
  556. outer.setCursorPos(width, 1)
  557. outer.write(scrollPos > 1 and "\30" or " ")
  558. outer.setCursorPos(width, height)
  559. outer.write(scrollPos < innerHeight - height and "\31" or " ")
  560. end
  561. end
  562.  
  563. return inner, scroll
  564. end
  565.  
  566. --- Creates a list of entries that can each be selected.
  567. ---@param win window The window to draw on
  568. ---@param x number The X coordinate of the inside of the box
  569. ---@param y number The Y coordinate of the inside of the box
  570. ---@param width number The width of the inner box
  571. ---@param height number The height of the inner box
  572. ---@param entries string[] A list of entries to show
  573. ---@param action function|string A function or `run` event that's called when a selection is made
  574. ---@param selectChangeAction function|string|nil A function or `run` event that's called when the current selection is changed
  575. ---@param fgColor color|nil The color of the text (defaults to white)
  576. ---@param bgColor color|nil The color of the background (defaults to black)
  577. function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectChangeAction, fgColor, bgColor)
  578. expect(1, win, "table")
  579. expect(2, x, "number")
  580. expect(3, y, "number")
  581. expect(4, width, "number")
  582. expect(5, height, "number")
  583. expect(6, entries, "table")
  584. expect(7, action, "function", "string")
  585. expect(8, selectChangeAction, "function", "string", "nil")
  586. fgColor = expect(9, fgColor, "number", "nil") or colors.white
  587. bgColor = expect(10, bgColor, "number", "nil") or colors.black
  588. if #entries == 0 then error("bad argument #6 (table must not be empty)", 2) end
  589. for i, v in ipairs(entries) do
  590. if type(v) ~= "string" then error("bad item " .. i .. " in entries table (expected string, got " .. type(v), 2) end
  591. end
  592. local entrywin = window.create(win, x, y, width, height)
  593. local selection, scroll = 1, 1
  594. local function drawEntries()
  595. entrywin.setVisible(false)
  596. entrywin.setBackgroundColor(bgColor)
  597. entrywin.clear()
  598. for i = scroll, scroll + height - 1 do
  599. local e = entries[i]
  600. if not e then break end
  601. entrywin.setCursorPos(2, i - scroll + 1)
  602. if i == selection then
  603. entrywin.setBackgroundColor(fgColor)
  604. entrywin.setTextColor(bgColor)
  605. else
  606. entrywin.setBackgroundColor(bgColor)
  607. entrywin.setTextColor(fgColor)
  608. end
  609. entrywin.clearLine()
  610. entrywin.write(#e > width - 1 and e:sub(1, width - 4) .. "..." or e)
  611. end
  612. entrywin.setBackgroundColor(bgColor)
  613. entrywin.setTextColor(fgColor)
  614. entrywin.setCursorPos(width, 1)
  615. entrywin.write("\30")
  616. entrywin.setCursorPos(width, height)
  617. entrywin.write("\31")
  618. entrywin.setVisible(true)
  619. end
  620. drawEntries()
  621. PrimeUI.addTask(function()
  622. while true do
  623. local event, key, cx, cy = os.pullEvent()
  624. if event == "key" then
  625. if key == keys.down and selection < #entries then
  626. selection = selection + 1
  627. if selection > scroll + height - 1 then scroll = scroll + 1 end
  628. if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
  629. elseif selectChangeAction then selectChangeAction(selection) end
  630. drawEntries()
  631. elseif key == keys.up and selection > 1 then
  632. selection = selection - 1
  633. if selection < scroll then scroll = scroll - 1 end
  634. if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
  635. elseif selectChangeAction then selectChangeAction(selection) end
  636. drawEntries()
  637. elseif key == keys.enter then
  638. if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries[selection])
  639. else action(entries[selection]) end
  640. end
  641. elseif event == "mouse_click" and key == 1 then
  642. local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
  643. if cx == wx + width - 1 then
  644. if cy == wy and selection > 1 then
  645. selection = selection - 1
  646. if selection < scroll then scroll = scroll - 1 end
  647. if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
  648. elseif selectChangeAction then selectChangeAction(selection) end
  649. drawEntries()
  650. elseif cy == wy + height - 1 and selection < #entries then
  651. selection = selection + 1
  652. if selection > scroll + height - 1 then scroll = scroll + 1 end
  653. if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
  654. elseif selectChangeAction then selectChangeAction(selection) end
  655. drawEntries()
  656. end
  657. elseif cx >= wx and cx < wx + width - 1 and cy >= wy and cy < wy + height then
  658. local sel = scroll + (cy - wy)
  659. if sel == selection then
  660. if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries[selection])
  661. else action(entries[selection]) end
  662. else
  663. selection = sel
  664. if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
  665. elseif selectChangeAction then selectChangeAction(selection) end
  666. drawEntries()
  667. end
  668. end
  669. elseif event == "mouse_scroll" then
  670. local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
  671. if cx >= wx and cx < wx + width and cy >= wy and cy < wy + height then
  672. if key < 0 and selection > 1 then
  673. selection = selection - 1
  674. if selection < scroll then scroll = scroll - 1 end
  675. if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
  676. elseif selectChangeAction then selectChangeAction(selection) end
  677. drawEntries()
  678. elseif key > 0 and selection < #entries then
  679. selection = selection + 1
  680. if selection > scroll + height - 1 then scroll = scroll + 1 end
  681. if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
  682. elseif selectChangeAction then selectChangeAction(selection) end
  683. drawEntries()
  684. end
  685. end
  686. end
  687. end
  688. end)
  689. end
  690.  
  691. --- Creates a text box that wraps text and can have its text modified later.
  692. ---@param win window The parent window of the text box
  693. ---@param x number The X position of the box
  694. ---@param y number The Y position of the box
  695. ---@param width number The width of the box
  696. ---@param height number The height of the box
  697. ---@param text string The initial text to draw
  698. ---@param fgColor color|nil The color of the text (defaults to white)
  699. ---@param bgColor color|nil The color of the background (defaults to black)
  700. ---@return function redraw A function to redraw the window with new contents
  701. function PrimeUI.textBox(win, x, y, width, height, text, fgColor, bgColor)
  702. expect(1, win, "table")
  703. expect(2, x, "number")
  704. expect(3, y, "number")
  705. expect(4, width, "number")
  706. expect(5, height, "number")
  707. expect(6, text, "string")
  708. fgColor = expect(7, fgColor, "number", "nil") or colors.white
  709. bgColor = expect(8, bgColor, "number", "nil") or colors.black
  710. local box = window.create(win, x, y, width, height)
  711. function box.getSize()
  712. return width, math.huge
  713. end
  714. local function redraw(_text)
  715. expect(1, _text, "string")
  716. box.setBackgroundColor(bgColor)
  717. box.setTextColor(fgColor)
  718. box.clear()
  719. box.setCursorPos(1, 1)
  720. local old = term.redirect(box)
  721. print(_text)
  722. term.redirect(old)
  723. end
  724. redraw(text)
  725. return redraw
  726. end
  727.  
  728. --- Draws a vertical line at a position with the specified height.
  729. ---@param win window The window to draw on
  730. ---@param x number The X position of the line
  731. ---@param y number The Y position of the top of the line
  732. ---@param height number The height of the line
  733. ---@param right boolean|nil Whether to align the line to the right instead of the left (defaults to false)
  734. ---@param fgColor color|nil The color of the line (defaults to white)
  735. ---@param bgColor color|nil The color of the background (defaults to black)
  736. function PrimeUI.verticalLine(win, x, y, height, right, fgColor, bgColor)
  737. expect(1, win, "table")
  738. expect(2, x, "number")
  739. expect(3, y, "number")
  740. expect(4, height, "number")
  741. right = expect(5, right, "boolean", "nil") or false
  742. fgColor = expect(6, fgColor, "number", "nil") or colors.white
  743. bgColor = expect(7, bgColor, "number", "nil") or colors.black
  744. win.setTextColor(right and bgColor or fgColor)
  745. win.setBackgroundColor(right and fgColor or bgColor)
  746. for j = 1, height do
  747. win.setCursorPos(x, y + j - 1)
  748. win.write("\x95")
  749. end
  750. end
  751.  
  752. ui = PrimeUI
  753.  
  754. local function box(terminal, x, y, width, height, color, cornerStyle)
  755. cornerStyle = cornerStyle or "square"
  756. terminal.setBackgroundColor(color)
  757. if cornerStyle == "square" then
  758. for i = y, y + height - 1 do
  759. terminal.setCursorPos(x, i)
  760. terminal.write(string.rep(" ", width))
  761. end
  762. elseif cornerStyle == "round" then
  763. terminal.setCursorPos(x + 1, y)
  764. terminal.write(string.rep(" ", width - 2))
  765. for i = y + 1, y + height - 2 do
  766. terminal.setCursorPos(x, i)
  767. terminal.write(string.rep(" ", width))
  768. end
  769. terminal.setCursorPos(x + 1, y + height - 1)
  770. terminal.write(string.rep(" ", width - 2))
  771. end
  772. end
  773. ui.box = box
  774.  
  775. local api_base_url = "https://ipod-2to6magyna-uc.a.run.app/"
  776.  
  777. local width, height = term.getSize()
  778.  
  779. local last_search_url = nil
  780. local search_results = nil
  781. local search_error = false
  782. local playing = false
  783. local queue = {}
  784. local now_playing = nil
  785. local looping = false
  786.  
  787. local playing_id = nil
  788. local last_download_url = nil
  789. local playing_status = 0
  790.  
  791. local player_handle = nil
  792. local start = nil
  793. local pcm = nil
  794. local size = nil
  795. local decoder = nil
  796. local needs_next_chunk = 0
  797. local buffer
  798.  
  799. local is_loading = false
  800. local is_error = false
  801.  
  802. local speakers = { peripheral.find("speaker") }
  803.  
  804. if #speakers == 0 then
  805. 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)
  806. end
  807.  
  808. local speaker = speakers[1]
  809.  
  810. os.startTimer(1)
  811.  
  812. -- ui helper functions
  813. local function playSong(song)
  814. now_playing = song
  815. playing = true
  816. playing_id = nil
  817. is_error = false
  818. end
  819.  
  820. local function stopPlayback()
  821. playing = false
  822. speaker.stop()
  823. playing_id = nil
  824. playing_status = 0
  825. needs_next_chunk = 0
  826. is_loading = false
  827. if player_handle then
  828. player_handle.close()
  829. player_handle = nil
  830. end
  831. end
  832.  
  833. local function togglePlayPause()
  834. if playing then
  835. stopPlayback()
  836. else
  837. if now_playing then
  838. playSong(now_playing)
  839. elseif #queue > 0 then
  840. local song = queue[1]
  841. table.remove(queue, 1)
  842. playSong(song)
  843. end
  844. end
  845. end
  846.  
  847. local function skipSong()
  848. stopPlayback()
  849. if #queue > 0 then
  850. local song = queue[1]
  851. table.remove(queue, 1)
  852. playSong(song)
  853. else
  854. now_playing = nil
  855. end
  856. end
  857.  
  858. local function toggleLoop()
  859. looping = not looping
  860. end
  861.  
  862. local function addToQueue(song, position)
  863. if position then
  864. table.insert(queue, position, song)
  865. else
  866. table.insert(queue, song)
  867. end
  868. end
  869.  
  870. local function removeFromQueue(position)
  871. if position and position >= 1 and position <= #queue then
  872. table.remove(queue, position)
  873. end
  874. end
  875.  
  876. -- Find a song's position in the queue by its id
  877. local function findInQueue(songId)
  878. for i, s in ipairs(queue) do
  879. if s.id == songId then
  880. return i
  881. end
  882. end
  883. return nil
  884. end
  885.  
  886. local function switchToNextSong()
  887. if #queue > 0 then
  888. now_playing = queue[1]
  889. table.remove(queue, 1)
  890. playing_id = nil
  891. else
  892. now_playing = nil
  893. playing = false
  894. playing_id = nil
  895. end
  896. os.queueEvent("song_changed") -- force redraw
  897. end
  898.  
  899. local function clearQueue()
  900. queue = {}
  901. end
  902.  
  903. local function searchMusic(query)
  904. last_search = query
  905. last_search_url = api_base_url .. "?search=" .. textutils.urlEncode(query)
  906. http.request(last_search_url)
  907. search_results = nil
  908. search_error = false
  909. end
  910.  
  911. -- custom palette based on Spotify one.
  912. local original_palette = {}
  913. local function initCustomPallete()
  914. for i = 1, 16 do
  915. original_palette[i] = {term.getPaletteColor(2^(i-1))}
  916. end
  917. term.setPaletteColor(colors.green, 0x1ED760)
  918. term.setPaletteColor(colors.lightGray, 0xb3b3b3)
  919. term.setPaletteColor(colors.gray, 0x212121)
  920. term.setPaletteColor(colors.purple, 0x457e59)
  921. term.setPaletteColor(colors.magenta, 0x62d089)
  922. term.setPaletteColor(colors.brown, 0x2e2e2e)
  923. end
  924.  
  925. local function fixString(str, limit)
  926. if not str then return "" end
  927. if #str <= limit then
  928. return str
  929. end
  930. return string.sub(str, 1, limit - 3) .. "..."
  931. end
  932.  
  933.  
  934. -- UI LOOP
  935.  
  936. ui.page = 1
  937.  
  938. local function redrawScreen()
  939. initCustomPallete()
  940.  
  941. while true do
  942. ui.clear()
  943. ui.borderBox(term.current(), 3, 2, width - 4, 1, colors.gray)
  944.  
  945. local isSmallScreen = width <= 30
  946.  
  947. -- ── Top bar ──────────────────────────────────────────────────────────
  948. if now_playing then
  949. if playing then
  950. if isSmallScreen then
  951. ui.button(term.current(), 4, 2, "S", "stop", colors.white, colors.red, colors.orange)
  952. else
  953. ui.button(term.current(), 4, 2, "Stop", "stop", colors.white, colors.red, colors.orange)
  954. end
  955. else
  956. ui.button(term.current(), 4, 2, "\16", "resume", colors.white, colors.green, colors.lightGray)
  957. ui.button(term.current(), 8, 2, "R", "clear", colors.white, colors.red, colors.orange)
  958. end
  959. if not isSmallScreen then
  960. local statusPrefix = is_loading and "[..] " or is_error and "[ERR] " or ""
  961. ui.label(term.current(), 12, 2, fixString(statusPrefix .. now_playing.name, 20), colors.white)
  962. ui.label(term.current(), 12 + string.len(fixString(statusPrefix .. now_playing.name, 20)) + 1, 2, "| " .. fixString(now_playing.artist, 14), colors.lightGray)
  963. else
  964. ui.label(term.current(), 8, 2, fixString(now_playing.name, 16), colors.white)
  965. end
  966. else
  967. ui.label(term.current(), 4, 2, "Musiclo", colors.green)
  968. if not isSmallScreen then
  969. ui.label(term.current(), 4 + string.len("Musiclo") + 2, 2, "| CC:T music player made easy", colors.lightGray)
  970. else
  971. ui.label(term.current(), 4 + string.len("Musiclo") + 1, 2, "| CC:T player", colors.lightGray)
  972. end
  973. end
  974.  
  975. local titleTruncateLimit = 41
  976. local artistTruncateLimit = 26
  977.  
  978. if isSmallScreen then
  979. titleTruncateLimit = 19
  980. artistTruncateLimit = 15
  981. end
  982.  
  983. -- ── Page 1: Queue ─────────────────────────────────────────────────────
  984. if ui.page == 1 then
  985. ui.borderBox(term.current(), 3, 5, width - 4, height - 6, colors.gray)
  986. ui.button(term.current(), width - 9, 4, "Search", "page.2", colors.white, colors.magenta, colors.purple)
  987. ui.keyAction(keys.enter, "page.2")
  988. ui.label(term.current(), 4, 4, "Queue", colors.white)
  989. ui.keyAction(keys.space, "toggle")
  990.  
  991. -- Bottom controls
  992. if looping then
  993. ui.button(term.current(), 4, height - 1, "Loop", "loop", colors.white, colors.magenta, colors.purple)
  994. else
  995. ui.button(term.current(), 4, height - 1, "Loop", "loop", colors.white, colors.gray, colors.lightGray)
  996. end
  997.  
  998. if #queue > 0 then
  999. ui.button(term.current(), 11, height - 1, "Skip", "skip", colors.white, colors.gray, colors.lightGray)
  1000. if isSmallScreen then
  1001. ui.button(term.current(), 18, height - 1, "Clr", "clear.q", colors.white, colors.red, colors.orange)
  1002. else
  1003. ui.button(term.current(), 18, height - 1, "Clear queue", "clear.q", colors.white, colors.red, colors.orange)
  1004. end
  1005. end
  1006.  
  1007. -- Scroll area
  1008. local scroller = ui.scrollBox(term.current(), 3, 5, width - 4, height - 6, 9000, true, true)
  1009.  
  1010. local y = 2
  1011. if #queue > 0 then
  1012. for i, song in ipairs(queue) do
  1013. ui.box(scroller, 1, y, width - 5, 5, colors.brown)
  1014. ui.label(scroller, 2, y + 1, fixString(song.name, titleTruncateLimit), colors.white, colors.brown)
  1015. ui.label(scroller, 2, y + 2, fixString(song.artist, artistTruncateLimit), colors.lightGray, colors.brown)
  1016.  
  1017. local rowY = isSmallScreen and (y + 1) or y
  1018. -- "Play now" button plays from queue, using queue index
  1019. ui.button(scroller, width - 20, rowY + 2, "Play", "qplay." .. i, colors.white, colors.magenta, colors.purple)
  1020. -- "Remove" removes from queue by index
  1021. ui.button(scroller, width - 13, rowY + 2, "Rem", "qrem." .. i, colors.white, colors.red, colors.orange)
  1022.  
  1023. y = y + 6
  1024. end
  1025. else
  1026. ui.centerLabel(scroller, 1, 5, width - 4, "No songs in queue", colors.lightGray)
  1027. ui.button(scroller, math.floor((width - 4 - string.len("Add song")) / 2) + 1, 7, "Add song", "page.2", colors.white, colors.gray, colors.lightGray)
  1028. end
  1029.  
  1030. -- ── Page 2: Search ────────────────────────────────────────────────────
  1031. elseif ui.page == 2 then
  1032. ui.borderBox(term.current(), 3, 5, width - 4, height - 6, colors.gray)
  1033. ui.button(term.current(), width - 10, 4, "Go back", "page.1", colors.white, colors.gray, colors.lightGray)
  1034. ui.label(term.current(), 4, 4, "Search", colors.white)
  1035. ui.label(term.current(), 4, 6, "Search on Youtube...", colors.lightGray)
  1036. ui.horizontalLine(term.current(), 3, 8, width - 4, colors.gray)
  1037.  
  1038. local scroller = ui.scrollBox(term.current(), 3, 9, width - 4, height - 10, 9000, true, true)
  1039.  
  1040. local y = 2
  1041. if search_error then
  1042. ui.centerLabel(scroller, 1, 5, width - 4, "Search failed!", colors.red)
  1043. elseif search_results then
  1044. if #search_results == 0 then
  1045. ui.centerLabel(scroller, 1, 5, width - 4, "No results found", colors.lightGray)
  1046. else
  1047. for i, song in ipairs(search_results) do
  1048. ui.box(scroller, 1, y, width - 6, 5, colors.brown)
  1049. ui.label(scroller, 2, y + 1, fixString(song.name, titleTruncateLimit), colors.white, colors.brown)
  1050. ui.label(scroller, 2, y + 2, fixString(song.artist, artistTruncateLimit), colors.lightGray, colors.brown)
  1051.  
  1052. local rowY = isSmallScreen and (y + 1) or y
  1053. -- Play immediately
  1054. ui.button(scroller, width - 21, rowY + 2, "Play", "splay." .. i, colors.white, colors.magenta, colors.purple)
  1055.  
  1056. -- Add or Remove from queue (check by song id)
  1057. local queuePos = findInQueue(song.id)
  1058. if queuePos then
  1059. ui.button(scroller, width - 14, rowY + 2, "Remove", "srem." .. i, colors.white, colors.red, colors.orange)
  1060. else
  1061. ui.button(scroller, width - 14, rowY + 2, "Add", "sadd." .. i, colors.white, colors.gray, colors.lightGray)
  1062. end
  1063.  
  1064. y = y + 6
  1065. end
  1066. end
  1067. end
  1068.  
  1069. ui.inputBox(term.current(), 4, 7, width - 7, "search", colors.white, colors.gray)
  1070. end
  1071.  
  1072. -- ── Event handling ────────────────────────────────────────────────────
  1073. local object, callback, text = ui.run()
  1074. term.clear()
  1075. term.setCursorPos(1, 1)
  1076.  
  1077. if object == "rerender" then
  1078. -- redraw
  1079. elseif object == "button" or object == "keyAction" then
  1080.  
  1081. if callback == "page.2" then
  1082. ui.page = 2
  1083.  
  1084. elseif callback == "page.1" then
  1085. ui.page = 1
  1086.  
  1087. -- ── Queue page actions ─────────────────────────────────────────
  1088. elseif callback:sub(1, 5) == "qplay" then
  1089. -- Play a specific song from the queue directly
  1090. local index = tonumber(callback:sub(7))
  1091. if index and queue[index] then
  1092. local song = queue[index]
  1093. table.remove(queue, index)
  1094. playSong(song)
  1095. end
  1096.  
  1097. elseif callback:sub(1, 4) == "qrem" then
  1098. -- Remove a song from the queue by index
  1099. local index = tonumber(callback:sub(6))
  1100. if index and queue[index] then
  1101. removeFromQueue(index)
  1102. end
  1103.  
  1104. -- ── Search page actions ────────────────────────────────────────
  1105. elseif callback:sub(1, 5) == "splay" then
  1106. -- Play a search result immediately (goes to now_playing)
  1107. local index = tonumber(callback:sub(7))
  1108. if index and search_results and search_results[index] then
  1109. playSong(search_results[index])
  1110. ui.page = 1
  1111. end
  1112.  
  1113. elseif callback:sub(1, 4) == "sadd" then
  1114. -- Add a search result to the queue
  1115. local index = tonumber(callback:sub(6))
  1116. if index and search_results and search_results[index] then
  1117. local song = search_results[index]
  1118. -- Only add if not already queued
  1119. if not findInQueue(song.id) then
  1120. addToQueue(song)
  1121. end
  1122. end
  1123.  
  1124. elseif callback:sub(1, 4) == "srem" then
  1125. -- Remove a search result's song from the queue (look up by id)
  1126. local index = tonumber(callback:sub(6))
  1127. if index and search_results and search_results[index] then
  1128. local queuePos = findInQueue(search_results[index].id)
  1129. if queuePos then
  1130. removeFromQueue(queuePos)
  1131. end
  1132. end
  1133.  
  1134. -- ── Playback controls ──────────────────────────────────────────
  1135. elseif callback == "stop" then
  1136. stopPlayback()
  1137.  
  1138. elseif callback == "resume" then
  1139. -- Resume the current song (restart it)
  1140. if now_playing then
  1141. playSong(now_playing)
  1142. end
  1143.  
  1144. elseif callback == "toggle" then
  1145. togglePlayPause()
  1146.  
  1147. elseif callback == "loop" then
  1148. toggleLoop()
  1149.  
  1150. elseif callback == "skip" then
  1151. skipSong()
  1152.  
  1153. elseif callback == "clear.q" then
  1154. clearQueue()
  1155.  
  1156. elseif callback == "clear" then
  1157. -- Clear now playing entirely
  1158. stopPlayback()
  1159. now_playing = nil
  1160. end
  1161.  
  1162. elseif object == "inputBox" and callback == "search" then
  1163. if text and text ~= "" then
  1164. searchMusic(text)
  1165. term.clear()
  1166. local sx, sy = term.getSize()
  1167. term.setTextColor(colors.lightGray)
  1168. term.setCursorPos(math.floor(sx / 2 - #"Fetching..." / 2), math.floor(sy / 2))
  1169. term.write("Fetching...")
  1170. ui.searchDone = false
  1171. repeat
  1172. sleep(0.1)
  1173. until ui.searchDone == true
  1174. ui.searchDone = false
  1175. end
  1176. end
  1177. end
  1178. end
  1179.  
  1180. local function audioLoop()
  1181. local chunkQueue = {} -- pre-buffered decoded chunks
  1182. local MAX_PREBUFFER = 3 -- keep up to 3 chunks decoded
  1183.  
  1184. while true do
  1185. sleep(0)
  1186.  
  1187. if playing and now_playing then
  1188. -- start new song
  1189. if playing_id ~= now_playing.id then
  1190. if player_handle then player_handle.close() player_handle = nil end
  1191. chunkQueue = {} -- clear prebuffer on song change
  1192. playing_id = now_playing.id
  1193. last_download_url = api_base_url .. "?v=2&id=" .. textutils.urlEncode(playing_id)
  1194. playing_status = 0
  1195. needs_next_chunk = 1
  1196. http.request({url = last_download_url, binary = true})
  1197. is_loading = true
  1198. is_error = false
  1199. end
  1200.  
  1201. -- feed speaker from prebuffer
  1202. if #chunkQueue > 0 then
  1203. local ok = speaker.playAudio(chunkQueue[1])
  1204. if ok then
  1205. table.remove(chunkQueue, 1)
  1206. end
  1207. end
  1208.  
  1209. -- fill prebuffer from stream
  1210. if playing_status == 1 and #chunkQueue < MAX_PREBUFFER then
  1211. local chunk = player_handle.read(size)
  1212. if not chunk then
  1213. -- end of stream
  1214. player_handle.close()
  1215. player_handle = nil
  1216. playing_status = 0
  1217. is_loading = false
  1218. if looping then
  1219. playing_id = nil
  1220. else
  1221. switchToNextSong()
  1222. end
  1223. else
  1224. if start then
  1225. chunk, start = start .. chunk, nil
  1226. size = size + 4
  1227. end
  1228. table.insert(chunkQueue, decoder(chunk))
  1229. end
  1230. end
  1231. end
  1232. end
  1233. end
  1234.  
  1235. -- Events
  1236. local function eventLoop()
  1237. while true do
  1238. local event, param1, param2 = os.pullEvent()
  1239.  
  1240. if event == "timer" then
  1241. os.startTimer(1)
  1242. end
  1243.  
  1244. if event == "http_success" then
  1245. local url = param1
  1246. local handle = param2
  1247.  
  1248. if url == last_search_url then
  1249. local raw = handle.readAll()
  1250. local parsed = textutils.unserialiseJSON(raw)
  1251. if parsed then
  1252. -- The API returns an extra first element, strip it
  1253. table.remove(parsed, 1)
  1254. search_results = parsed
  1255. search_error = false
  1256. else
  1257. search_results = {}
  1258. search_error = true
  1259. end
  1260. ui.searchDone = true
  1261. end
  1262.  
  1263. if url == last_download_url then
  1264. player_handle = handle
  1265. start = handle.read(4)
  1266. size = 16 * 1024 - 4
  1267. if start == "RIFF" then
  1268. error("WAV not supported!")
  1269. end
  1270. playing_status = 1
  1271. is_loading = false
  1272. decoder = require "cc.audio.dfpwm".make_decoder()
  1273. end
  1274. end
  1275.  
  1276. if event == "http_failure" then
  1277. local url = param1
  1278.  
  1279. if url == last_search_url then
  1280. search_error = true
  1281. search_results = nil
  1282. ui.searchDone = true
  1283. end
  1284.  
  1285. if url == last_download_url then
  1286. is_loading = false
  1287. is_error = true
  1288. playing_status = 0
  1289. needs_next_chunk = 0
  1290. playing_id = nil
  1291.  
  1292. -- Skip to next in queue on error
  1293. if #queue > 0 then
  1294. now_playing = queue[1]
  1295. table.remove(queue, 1)
  1296. playing_id = nil
  1297. is_error = false
  1298. else
  1299. now_playing = nil
  1300. playing = false
  1301. end
  1302. end
  1303. end
  1304.  
  1305. if event == "song_changed" then
  1306. PrimeUI.resolve("rerender", "song_changed")
  1307. end
  1308. end
  1309. end
  1310.  
  1311. parallel.waitForAny(audioLoop, eventLoop, redrawScreen)
  1312.  
  1313. -- Cleanup
  1314. for i = 1, 16 do
  1315. local c = 2^(i-1)
  1316. if original_palette[i] then
  1317. term.setPaletteColor(c, table.unpack(original_palette[i]))
  1318. end
  1319. end
  1320. term.setCursorBlink(false)
  1321. term.clear()
  1322. term.setCursorPos(1, 1)
  1323.  
Advertisement
Comments
  • User was banned
Add Comment
Please, Sign In to add comment