|
| 1 | +-- This is released into the public domain. |
| 2 | +-- No warranty is provided, implied or otherwise. |
| 3 | + |
| 4 | +-- app-tapedeck.lua : Computronics Tape interface. |
| 5 | +-- Added note: Computerized record discs aren't available, so it can't be called vinylscratch. |
| 6 | +-- Authors: 20kdc |
| 7 | + |
| 8 | +local tapes = {} |
| 9 | +for v in neo.requireAccess("c.tape_drive", "tapedrives").list() do |
| 10 | + table.insert(tapes, v) |
| 11 | +end |
| 12 | + |
| 13 | +local tapeRate = 4096 |
| 14 | + |
| 15 | +local event = require("event")(neo) |
| 16 | +local neoux = require("neoux")(event, neo) |
| 17 | + |
| 18 | +-- There's no way to get these, so they get reset |
| 19 | +local pcvals = {vol = 100, spd = 100} |
| 20 | +local function pcbox(x, y, low, high, id, fun) |
| 21 | + return neoux.tcfield(x, y, 5, function (tx) |
| 22 | + if tx then |
| 23 | + pcvals[id] = math.min(math.max(0, math.floor(tonumber(tx) or 0)), high) |
| 24 | + fun(math.max(pcvals[id], low) / 100) |
| 25 | + end |
| 26 | + return tostring(pcvals[id]) |
| 27 | + end) |
| 28 | +end |
| 29 | + |
| 30 | +local window |
| 31 | +local running = true |
| 32 | +local focused = true |
| 33 | + |
| 34 | +local updateTick |
| 35 | + |
| 36 | +local downloadCancelled = false |
| 37 | + |
| 38 | +local genPlayer -- used to return to player |
| 39 | + |
| 40 | +local function genDownloading(inst) |
| 41 | + local lclLabelText = {"downloading..."} |
| 42 | + local lclLabel = neoux.tcrawview(1, 1, lclLabelText) |
| 43 | + local thr = { |
| 44 | + "/", |
| 45 | + "-", |
| 46 | + "\\", |
| 47 | + "|" |
| 48 | + } |
| 49 | + local thri = 0 |
| 50 | + updateTick = function () |
| 51 | + lclLabelText[1] = "downloading... " .. (inst.getPosition() / (1024 * 1024)) .. "MB " .. thr[(thri % #thr) + 1] |
| 52 | + thri = thri + 1 |
| 53 | + lclLabel.update(window) |
| 54 | + end |
| 55 | + return 40, 1, nil, neoux.tcwindow(40, 1, { |
| 56 | + lclLabel |
| 57 | + }, function (w) |
| 58 | + downloadCancelled = true |
| 59 | + end, 0xFFFFFF, 0) |
| 60 | +end |
| 61 | + |
| 62 | +local function doINetThing(inet, url, inst) |
| 63 | + inet = inet.list()() |
| 64 | + assert(inet, "No available card") |
| 65 | + inst.stop() |
| 66 | + inst.seek(-inst.getSize()) |
| 67 | + downloadCancelled = false |
| 68 | + downloadPercent = 0 |
| 69 | + window.reset(genDownloading(inst)) |
| 70 | + local req = assert(inet.request(url)) |
| 71 | + req.finishConnect() |
| 72 | + local tapePos = 0 |
| 73 | + local tapeSize = inst.getSize() |
| 74 | + while (not downloadCancelled) and tapePos < tapeSize do |
| 75 | + local n, n2 = req.read(neo.readBufSize) |
| 76 | + if not n then |
| 77 | + if n2 then |
| 78 | + req.close() |
| 79 | + error(n2) |
| 80 | + end |
| 81 | + break |
| 82 | + elseif n == "" then |
| 83 | + event.sleepTo(os.uptime() + 0.05) |
| 84 | + else |
| 85 | + inst.write(n) |
| 86 | + tapePos = tapePos + #n |
| 87 | + end |
| 88 | + end |
| 89 | + req.close() |
| 90 | + inst.seek(-inst.getSize()) |
| 91 | +end |
| 92 | + |
| 93 | +local function genWeb(inst) |
| 94 | + updateTick = nil |
| 95 | + local url = "" |
| 96 | + local lockout = false |
| 97 | + return 40, 3, nil, neoux.tcwindow(40, 3, { |
| 98 | + neoux.tcrawview(1, 1, {"URL to write to tape?"}), |
| 99 | + neoux.tcfield(1, 2, 40, function (t) |
| 100 | + url = t or url |
| 101 | + return url |
| 102 | + end), |
| 103 | + neoux.tcbutton(1, 3, "Download & Write", function (w) |
| 104 | + lockout = true |
| 105 | + local inet = neo.requestAccess("c.internet") |
| 106 | + lockout = false |
| 107 | + if inet then |
| 108 | + local ok, err = pcall(doINetThing, inet, url, inst) |
| 109 | + if not ok then |
| 110 | + neoux.startDialog("Couldn't download: " .. tostring(err), "error") |
| 111 | + end |
| 112 | + end |
| 113 | + w.reset(genPlayer(inst)) |
| 114 | + end) |
| 115 | + }, function (w) |
| 116 | + w.reset(genPlayer(inst)) |
| 117 | + end, 0xFFFFFF, 0) |
| 118 | +end |
| 119 | + |
| 120 | +-- The actual main UI -- |
| 121 | +genPlayer = function (inst) |
| 122 | + local cachedLabel = inst.getLabel() or "" |
| 123 | + local cachedState = inst.getState() |
| 124 | + local function pausePlay() |
| 125 | + if inst.getState() == "PLAYING" then |
| 126 | + inst.stop() |
| 127 | + else |
| 128 | + inst.play() |
| 129 | + end |
| 130 | + window.reset(genPlayer(inst)) |
| 131 | + end |
| 132 | + -- Common code for reading/writing tapes. |
| 133 | + -- Note that it tries to allow playback to resume later. |
| 134 | + local function rwButton(mode) |
| 135 | + local fh = neoux.fileDialog(mode) |
| 136 | + if not fh then return end |
| 137 | + inst.stop() |
| 138 | + local sp = inst.getPosition() |
| 139 | + local tapeSize = inst.getSize() |
| 140 | + inst.seek(-tapeSize) |
| 141 | + local tapePos = 0 |
| 142 | + while tapePos < tapeSize do |
| 143 | + if mode then |
| 144 | + local data = inst.read(neo.readBufSize) |
| 145 | + if not data then break end |
| 146 | + tapePos = tapePos + #data |
| 147 | + local res, ifo = fh.write(data) |
| 148 | + if not res then |
| 149 | + neoux.startDialog(tostring(ifo), "issue") |
| 150 | + break |
| 151 | + end |
| 152 | + else |
| 153 | + local data = fh.read(neo.readBufSize) |
| 154 | + if not data then break end |
| 155 | + tapePos = tapePos + #data |
| 156 | + inst.write(data) |
| 157 | + end |
| 158 | + end |
| 159 | + inst.seek(-tapeSize) |
| 160 | + inst.seek(sp) |
| 161 | + fh.close() |
| 162 | + end |
| 163 | + local elems = { |
| 164 | + neoux.tcrawview(1, 1, { |
| 165 | + "Label:", |
| 166 | + "Contents:" |
| 167 | + }), |
| 168 | + neoux.tcfield(7, 1, 34, function (tx) |
| 169 | + if tx then |
| 170 | + inst.setLabel(tx) |
| 171 | + cachedLabel = tx |
| 172 | + end |
| 173 | + return cachedLabel |
| 174 | + end), |
| 175 | + { |
| 176 | + x = 1, |
| 177 | + y = 5, |
| 178 | + w = 40, |
| 179 | + h = 1, |
| 180 | + selectable = true, |
| 181 | + line = function (w, x, y, lined, bg, fg, selected) |
| 182 | + local lx = "" |
| 183 | + local pos = inst.getPosition() |
| 184 | + local sz = inst.getSize() |
| 185 | + if inst.isReady() then |
| 186 | + -- Show a bar |
| 187 | + local tick = sz / 23 |
| 188 | + for i = 1, 23 do |
| 189 | + local alpos = (tick * i) - (tick / 2) |
| 190 | + if pos > alpos then |
| 191 | + lx = lx .. "=" |
| 192 | + else |
| 193 | + lx = lx .. "-" |
| 194 | + end |
| 195 | + end |
| 196 | + else |
| 197 | + lx = "NO TAPE HERE." |
| 198 | + end |
| 199 | + local sec = pos / tapeRate |
| 200 | + local secz = sz / tapeRate |
| 201 | + lx = lx .. string.format(" %03i:%02i / %03i:%02i ", |
| 202 | + math.floor(sec / 60), math.floor(sec) % 60, |
| 203 | + math.floor(secz / 60), math.floor(secz) % 60) |
| 204 | + if selected then bg, fg = fg, bg end |
| 205 | + window.span(x, y, lx, bg, fg) |
| 206 | + end, |
| 207 | + key = function (w, update, a, b, c, kf) |
| 208 | + local amount = tapeRate * 10 |
| 209 | + if kf.shift or kf.rshift then |
| 210 | + amount = amount * 24 |
| 211 | + end |
| 212 | + if c then |
| 213 | + if a == 32 then |
| 214 | + pausePlay() |
| 215 | + elseif b == 203 then |
| 216 | + inst.seek(-amount) |
| 217 | + update() |
| 218 | + return true |
| 219 | + elseif b == 205 then |
| 220 | + inst.seek(amount) |
| 221 | + update() |
| 222 | + return true |
| 223 | + end |
| 224 | + end |
| 225 | + end |
| 226 | + }, |
| 227 | + neoux.tcrawview(33, 3, { |
| 228 | + "% Volume" |
| 229 | + }), |
| 230 | + neoux.tcrawview(20, 3, { |
| 231 | + "% Speed" |
| 232 | + }), |
| 233 | + pcbox(15, 3, 25, 200, "spd", inst.setSpeed), |
| 234 | + pcbox(28, 3, 0, 100, "vol", inst.setVolume), |
| 235 | + neoux.tcrawview(1, 4, { |
| 236 | + "Seeker: use ◃/▹ (shift goes faster)" |
| 237 | + }), |
| 238 | + neoux.tcbutton(1, 3, "«", function (w) |
| 239 | + inst.seek(-inst.getSize()) |
| 240 | + end), |
| 241 | + neoux.tcbutton(11, 3, "»", function (w) |
| 242 | + inst.seek(inst.getSize()) |
| 243 | + end), |
| 244 | + neoux.tcbutton(4, 3, ((inst.getState() == "PLAYING") and "Pause") or "Play", function (w) |
| 245 | + pausePlay() |
| 246 | + end), |
| 247 | + -- R/W buttons |
| 248 | + neoux.tcbutton(11, 2, "Read", function (w) |
| 249 | + rwButton(true) |
| 250 | + end), |
| 251 | + neoux.tcbutton(17, 2, "Write", function (w) |
| 252 | + rwButton(false) |
| 253 | + end), |
| 254 | + neoux.tcbutton(24, 2, "Write From Web", function (w) |
| 255 | + w.reset(genWeb(inst)) |
| 256 | + end) |
| 257 | + } |
| 258 | + updateTick = function () |
| 259 | + local lcl = cachedLabel |
| 260 | + cachedLabel = inst.getLabel() or "" |
| 261 | + elems[3].update(window) |
| 262 | + if inst.getState() ~= cachedState then |
| 263 | + window.reset(genPlayer(inst)) |
| 264 | + elseif lcl ~= cachedLabel then |
| 265 | + elems[2].update(window) |
| 266 | + end |
| 267 | + end |
| 268 | + local n = neoux.tcwindow(40, 5, elems, function (w) |
| 269 | + updateTick = nil |
| 270 | + running = false |
| 271 | + w.close() |
| 272 | + end, 0xFFFFFF, 0) |
| 273 | + return 40, 5, inst.address, function (a, ...) |
| 274 | + if a == "focus" then |
| 275 | + focused = (...) or true |
| 276 | + end |
| 277 | + return n(a, ...) |
| 278 | + end |
| 279 | +end |
| 280 | +local function genList() |
| 281 | + local elems = {} |
| 282 | + for k, v in ipairs(tapes) do |
| 283 | + elems[k] = neoux.tcbutton(1, k, v.address:sub(1, 38), function (w) |
| 284 | + window.reset(genPlayer(v)) |
| 285 | + end) |
| 286 | + end |
| 287 | + tapes = nil |
| 288 | + return 40, #elems, nil, neoux.tcwindow(40, #elems, elems, function (w) |
| 289 | + running = false |
| 290 | + w.close() |
| 291 | + end, 0xFFFFFF, 0) |
| 292 | +end |
| 293 | + |
| 294 | + |
| 295 | +window = neoux.create(genList()) |
| 296 | + |
| 297 | +-- Timer for time update |
| 298 | +local function tick() |
| 299 | + if updateTick then |
| 300 | + updateTick() |
| 301 | + end |
| 302 | + event.runAt(os.uptime() + ((focused and 1) or 10), tick) |
| 303 | +end |
| 304 | +event.runAt(0, tick) |
| 305 | + |
| 306 | +while running do |
| 307 | + event.pull() |
| 308 | +end |
0 commit comments