2023-08-19 04:48:46 +02:00
|
|
|
json = require "scripts.core.libb.json"
|
2022-02-20 17:27:27 +01:00
|
|
|
local header = {}
|
|
|
|
header["user-agent"] = "unnamed_sdvx_clone"
|
|
|
|
|
|
|
|
local jacketFallback = gfx.CreateSkinImage("song_select/loading.png", 0)
|
|
|
|
local diffColors = {{50,50,127}, {50,127,50}, {127,50,50}, {127, 50, 127}}
|
|
|
|
local entryW = 770
|
|
|
|
local entryH = 320
|
|
|
|
local resX,resY = game.GetResolution()
|
|
|
|
local xCount = math.max(1, math.floor(resX / entryW))
|
|
|
|
local yCount = math.max(1, math.floor(resY / entryH))
|
|
|
|
local xOffset = (resX - xCount * entryW) / 2
|
|
|
|
local cursorPos = 0
|
|
|
|
local cursorPosX = 0
|
|
|
|
local cursorPosY = 0
|
|
|
|
local displayCursorPosX = 0
|
|
|
|
local displayCursorPosY = 0
|
|
|
|
local nextUrl = "https://ksm.dev/app/songs"
|
|
|
|
local screenState = 0 --0 = normal, 1 = level, 2 = sorting
|
|
|
|
local loading = true
|
|
|
|
local downloaded = {}
|
|
|
|
local songs = {}
|
|
|
|
local selectedLevels = {}
|
|
|
|
local selectedSorting = "Uploaded"
|
|
|
|
local lastPlaying = nil
|
|
|
|
for i = 1, 20 do
|
|
|
|
selectedLevels[i] = false
|
|
|
|
end
|
|
|
|
|
|
|
|
local cachepath = path.Absolute("skins/" .. game.GetSkin() .. "/nautica.json")
|
|
|
|
local levelcursor = 0
|
|
|
|
local sortingcursor = 0
|
|
|
|
local sortingOptions = {"Uploaded", "Oldest"}
|
|
|
|
local needsReload = false
|
|
|
|
|
|
|
|
function addsong(song)
|
|
|
|
if song.jacket_url ~= nil then
|
|
|
|
song.jacket = gfx.LoadWebImageJob(song.jacket_url, jacketFallback, 250, 250)
|
|
|
|
else
|
|
|
|
song.jacket = jacketFallback
|
|
|
|
end
|
|
|
|
if downloaded[song.id] then
|
|
|
|
song.status = "Downloaded"
|
|
|
|
end
|
|
|
|
table.insert(songs, song)
|
|
|
|
end
|
|
|
|
local yOffset = 0
|
|
|
|
local backgroundImage = gfx.CreateSkinImage("bg.png", 1);
|
|
|
|
|
|
|
|
dlcache = io.open(cachepath, "r")
|
|
|
|
if dlcache then
|
|
|
|
downloaded = json.decode(dlcache:read("*all"))
|
|
|
|
dlcache:close()
|
|
|
|
end
|
|
|
|
function encodeURI(str)
|
|
|
|
if (str) then
|
|
|
|
str = string.gsub(str, "\n", "\r\n")
|
|
|
|
str = string.gsub(str, "([^%w ])",
|
|
|
|
function (c)
|
|
|
|
local dontChange = "-/_:."
|
|
|
|
for i = 1, #dontChange do
|
|
|
|
if c == dontChange:sub(i,i) then return c end
|
|
|
|
end
|
|
|
|
return string.format ("%%%02X", string.byte(c))
|
|
|
|
end)
|
|
|
|
str = string.gsub(str, " ", "%%20")
|
|
|
|
end
|
|
|
|
return str
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function gotSongsCallback(response)
|
|
|
|
if response.status ~= 200 then
|
|
|
|
error()
|
|
|
|
return
|
|
|
|
end
|
|
|
|
local jsondata = json.decode(response.text)
|
|
|
|
for i,song in ipairs(jsondata.data) do
|
|
|
|
addsong(song)
|
|
|
|
end
|
|
|
|
nextUrl = jsondata.links.next
|
|
|
|
loading = false
|
|
|
|
end
|
|
|
|
|
|
|
|
Http.GetAsync(nextUrl, header, gotSongsCallback)
|
|
|
|
|
|
|
|
|
|
|
|
function render_song(song, x,y)
|
|
|
|
gfx.Save()
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
|
|
|
|
gfx.Translate(x,y)
|
|
|
|
gfx.Scissor(0,0,750,300)
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.FillColor(0,0,0,140)
|
|
|
|
gfx.Rect(0,0,750,300)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.FontSize(30)
|
|
|
|
gfx.Text(song.title, 2,2)
|
|
|
|
gfx.FontSize(24)
|
|
|
|
gfx.Text(song.artist, 2,26)
|
|
|
|
if song.jacket_url ~= nil and song.jacket == jacketFallback then
|
|
|
|
song.jacket = gfx.LoadWebImageJob(song.jacket_url, jacketFallback, 250, 250)
|
|
|
|
end
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.ImageRect(0, 50, 250, 250, song.jacket, 1, 0)
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(250,50,500,250)
|
|
|
|
gfx.FillColor(55,55,55,128)
|
|
|
|
gfx.Fill()
|
|
|
|
for i, diff in ipairs(song.charts) do
|
|
|
|
local col = diffColors[diff.difficulty]
|
|
|
|
local diffY = 50 + 250/4 * (diff.difficulty - 1)
|
|
|
|
gfx.BeginPath()
|
|
|
|
|
|
|
|
gfx.Rect(250,diffY, 500, 250 / 4)
|
|
|
|
gfx.FillColor(col[1], col[2], col[3])
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.FontSize(40)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_MIDDLE)
|
|
|
|
gfx.Text(string.format("%d Effected by %s", diff.level, diff.effector), 255, diffY + 250 / 8)
|
|
|
|
end
|
|
|
|
if downloaded[song.id] then
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(0,0,750,300)
|
|
|
|
gfx.FillColor(0,0,0,127)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
|
|
|
|
gfx.FontSize(60)
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.Text(downloaded[song.id], 375, 150)
|
|
|
|
elseif song.status then
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(0,0,750,300)
|
|
|
|
gfx.FillColor(0,0,0,127)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
|
|
|
|
gfx.FontSize(60)
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.Text(song.status, 375, 150)
|
|
|
|
end
|
|
|
|
gfx.ResetScissor()
|
|
|
|
gfx.Restore()
|
|
|
|
end
|
|
|
|
|
|
|
|
function load_more()
|
|
|
|
if nextUrl ~= nil and not loading then
|
|
|
|
Http.GetAsync(nextUrl, header, gotSongsCallback)
|
|
|
|
loading = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function render_cursor()
|
|
|
|
local x = displayCursorPosX * entryW
|
|
|
|
local y = displayCursorPosY * entryH
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(x,y,750,300)
|
|
|
|
gfx.StrokeColor(255,128,0)
|
|
|
|
gfx.StrokeWidth(5)
|
|
|
|
gfx.Stroke()
|
|
|
|
end
|
|
|
|
|
|
|
|
function render_loading()
|
|
|
|
if not loading then return end
|
|
|
|
gfx.Save()
|
|
|
|
gfx.ResetTransform()
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.MoveTo(resX, resY)
|
|
|
|
gfx.LineTo(resX - 350, resY)
|
|
|
|
gfx.LineTo(resX - 300, resY - 50)
|
|
|
|
gfx.LineTo(resX, resY - 50)
|
|
|
|
gfx.ClosePath()
|
|
|
|
gfx.FillColor(33,33,33)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT, gfx.TEXT_ALIGN_BOTTOM)
|
|
|
|
gfx.FontSize(70)
|
|
|
|
gfx.Text("LOADING...", resX - 20, resY - 3)
|
|
|
|
gfx.Restore()
|
|
|
|
end
|
|
|
|
|
|
|
|
function render_hotkeys()
|
|
|
|
gfx.Save()
|
|
|
|
gfx.ResetTransform()
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.FillColor(0,0,0,240)
|
|
|
|
gfx.Rect(0,resY - 50, resX, 50)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.FontSize(30)
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_BOTTOM)
|
|
|
|
gfx.Text("FXR: Sorting", resX/2 + 20, resY - 10)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_RIGHT, gfx.TEXT_ALIGN_BOTTOM)
|
|
|
|
gfx.Text("FXL: Levels", resX/2 - 20, resY - 10)
|
|
|
|
gfx.Restore()
|
|
|
|
end
|
|
|
|
|
|
|
|
function render_info()
|
|
|
|
gfx.Save()
|
|
|
|
gfx.ResetTransform()
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.MoveTo(0, resY)
|
|
|
|
gfx.LineTo(350, resY)
|
|
|
|
gfx.LineTo(300, resY - 50)
|
|
|
|
gfx.LineTo(0, resY - 50)
|
|
|
|
gfx.ClosePath()
|
|
|
|
gfx.FillColor(33,33,33)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT, gfx.TEXT_ALIGN_BOTTOM)
|
|
|
|
gfx.FontSize(70)
|
|
|
|
gfx.Text("Nautica", 3, resY - 3)
|
|
|
|
local xmin,ymin,xmax,ymax = gfx.TextBounds(3, resY - 3, "Nautica")
|
|
|
|
gfx.FontSize(20)
|
|
|
|
gfx.Text("https://ksm.dev/", xmax + 13, resY - 3)
|
|
|
|
gfx.Restore()
|
|
|
|
end
|
|
|
|
|
|
|
|
function render(deltaTime)
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.ImageRect(0, 0, resX, resY, backgroundImage, 1, 0);
|
|
|
|
gfx.LoadSkinFont("NotoSans-Regular.ttf");
|
|
|
|
displayCursorPosX = displayCursorPosX - (displayCursorPosX - cursorPosX) * deltaTime * 10
|
|
|
|
displayCursorPosY = displayCursorPosY - (displayCursorPosY - cursorPosY) * deltaTime * 10
|
|
|
|
if displayCursorPosY - yOffset > yCount - 1 then --scrolling down
|
|
|
|
yOffset = yOffset - (yOffset - displayCursorPosY) - yCount + 1
|
|
|
|
elseif displayCursorPosY - yOffset < 0 then
|
|
|
|
yOffset = yOffset - (yOffset - displayCursorPosY)
|
|
|
|
end
|
|
|
|
gfx.Translate(xOffset, 50 - yOffset * entryH)
|
|
|
|
for i, song in ipairs(songs) do
|
|
|
|
if math.abs(cursorPos - i) <= xCount * yCount + xCount then
|
|
|
|
i = i - 1
|
|
|
|
local x = entryW * (i % xCount)
|
|
|
|
local y = math.floor(i / xCount) * entryH
|
|
|
|
render_song(song, x, y)
|
|
|
|
if math.abs(#songs - i) < 4 then load_more() end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
render_cursor()
|
|
|
|
if needsReload then reload_songs() end
|
|
|
|
if screenState == 1 then render_level_filters()
|
|
|
|
elseif screenState == 2 then render_sorting_selection()
|
|
|
|
end
|
|
|
|
render_hotkeys()
|
|
|
|
render_loading()
|
|
|
|
render_info()
|
|
|
|
end
|
|
|
|
|
|
|
|
function archive_callback(entries, id)
|
|
|
|
game.Log("Listing entries for " .. id, 0)
|
|
|
|
local songsfolder = dlScreen.GetSongsPath()
|
|
|
|
res = {}
|
|
|
|
folders = { songsfolder .. "/nautica/" }
|
|
|
|
local hasFolder = false
|
|
|
|
for i, entry in ipairs(entries) do
|
|
|
|
for j = 1, #entry do
|
|
|
|
if entry:sub(j,j) == '/' then
|
|
|
|
hasFolder = true
|
|
|
|
table.insert(folders, songsfolder .. "/nautica/" .. entry:sub(1,j))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
game.Log(entry, 0)
|
|
|
|
res[entry] = songsfolder .. "/nautica/" .. entry
|
|
|
|
end
|
|
|
|
|
|
|
|
if not hasFolder then
|
|
|
|
for i, entry in ipairs(entries) do
|
|
|
|
res[entry] = songsfolder .. "/nautica/" .. id .. "/" .. entry
|
|
|
|
end
|
|
|
|
table.insert(folders, songsfolder .. "/nautica/" .. id .. "/")
|
|
|
|
end
|
|
|
|
downloaded[id] = "Downloaded"
|
|
|
|
res[".folders"] = table.concat(folders, "|")
|
|
|
|
return res
|
|
|
|
end
|
|
|
|
|
|
|
|
function reload_songs()
|
|
|
|
needsReload = true
|
|
|
|
if loading then return end
|
|
|
|
local useLevels = false
|
|
|
|
local levelarr = {}
|
|
|
|
|
|
|
|
for i,value in ipairs(selectedLevels) do
|
|
|
|
if value then
|
|
|
|
useLevels = true
|
|
|
|
table.insert(levelarr, i)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
nextUrl = string.format("https://ksm.dev/app/songs?sort=%s", selectedSorting:lower())
|
|
|
|
if useLevels then
|
|
|
|
nextUrl = nextUrl .. "&levels=" .. table.concat(levelarr, ",")
|
|
|
|
end
|
|
|
|
songs = {}
|
|
|
|
cursorPos = 0
|
|
|
|
cursorPosX = 0
|
|
|
|
cursorPosY = 0
|
|
|
|
displayCursorPosX = 0
|
|
|
|
displayCursorPosY = 0
|
|
|
|
load_more()
|
|
|
|
game.Log(nextUrl, 0)
|
|
|
|
needsReload = false
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
function button_pressed(button)
|
|
|
|
if button == game.BUTTON_STA then
|
|
|
|
if screenState == 0 then
|
|
|
|
local song = songs[cursorPos + 1]
|
|
|
|
if song == nil then return end
|
|
|
|
dlScreen.DownloadArchive(encodeURI(song.cdn_download_url), header, song.id, archive_callback)
|
|
|
|
downloaded[song.id] = "Downloading..."
|
|
|
|
elseif screenState == 1 then
|
|
|
|
if selectedLevels[levelcursor + 1] then
|
|
|
|
selectedLevels[levelcursor + 1] = false
|
|
|
|
else
|
|
|
|
selectedLevels[levelcursor + 1] = true
|
|
|
|
end
|
|
|
|
reload_songs()
|
|
|
|
elseif screenState == 2 then
|
|
|
|
selectedSorting = sortingOptions[sortingcursor + 1]
|
|
|
|
reload_songs()
|
|
|
|
end
|
|
|
|
|
|
|
|
elseif button == game.BUTTON_BTA then
|
|
|
|
if screenState == 0 then
|
|
|
|
local song = songs[cursorPos + 1]
|
|
|
|
if song == nil then return end
|
|
|
|
dlScreen.PlayPreview(encodeURI(song.preview_url), header, song.id)
|
|
|
|
song.status = "Playing"
|
|
|
|
if lastPlaying ~=nil then
|
|
|
|
lastPlaying.status = nil
|
|
|
|
end
|
|
|
|
lastPlaying = song
|
|
|
|
end
|
|
|
|
|
|
|
|
elseif button == game.BUTTON_FXL then
|
|
|
|
if screenState ~= 1 then
|
|
|
|
screenState = 1
|
|
|
|
else
|
|
|
|
screenState = 0
|
|
|
|
end
|
|
|
|
elseif button == game.BUTTON_FXR then
|
|
|
|
if screenState ~= 2 then
|
|
|
|
screenState = 2
|
|
|
|
else
|
|
|
|
screenState = 0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function key_pressed(key)
|
|
|
|
if key == 27 then --escape pressed
|
|
|
|
dlcache = io.open(cachepath, "w")
|
|
|
|
dlcache:write(json.encode(downloaded))
|
|
|
|
dlcache:close()
|
|
|
|
dlScreen.Exit()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function advance_selection(steps)
|
|
|
|
if screenState == 0 and #songs > 0 then
|
|
|
|
cursorPos = (cursorPos + steps) % #songs
|
|
|
|
cursorPosX = cursorPos % xCount
|
|
|
|
cursorPosY = math.floor(cursorPos / xCount)
|
|
|
|
if cursorPos > #songs - 6 then
|
|
|
|
load_more()
|
|
|
|
end
|
|
|
|
elseif screenState == 1 then
|
|
|
|
levelcursor = (levelcursor + steps) % 20
|
|
|
|
elseif screenState == 2 then
|
|
|
|
sortingcursor = (sortingcursor + steps) % #sortingOptions
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function render_level_filters()
|
|
|
|
gfx.Save()
|
|
|
|
gfx.ResetTransform()
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(0,0, resX, resY)
|
|
|
|
gfx.FillColor(0,0,0,200)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
|
|
|
|
gfx.FontSize(60)
|
|
|
|
gfx.Text("Level filters:", 10, 10)
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(resX/2 - 30, resY/2 - 22, 60, 44)
|
|
|
|
gfx.StrokeColor(255,128,0)
|
|
|
|
gfx.StrokeWidth(2)
|
|
|
|
gfx.Stroke()
|
|
|
|
gfx.FontSize(40)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
|
|
|
|
for i = 1, 20 do
|
|
|
|
y = (resY/2) + (i - (levelcursor + 1)) * 40
|
|
|
|
if selectedLevels[i] then gfx.FillColor(255,255,255) else gfx.FillColor(127,127,127) end
|
|
|
|
gfx.Text(tostring(i), resX/2, y)
|
|
|
|
end
|
|
|
|
gfx.Restore()
|
|
|
|
end
|
|
|
|
|
|
|
|
function render_sorting_selection()
|
|
|
|
gfx.Save()
|
|
|
|
gfx.ResetTransform()
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(0,0, resX, resY)
|
|
|
|
gfx.FillColor(0,0,0,200)
|
|
|
|
gfx.Fill()
|
|
|
|
gfx.FillColor(255,255,255)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_LEFT + gfx.TEXT_ALIGN_TOP)
|
|
|
|
gfx.FontSize(60)
|
|
|
|
gfx.Text("Sorting method:", 10, 10)
|
|
|
|
gfx.BeginPath()
|
|
|
|
gfx.Rect(resX/2 - 75, resY/2 - 22, 150, 44)
|
|
|
|
gfx.StrokeColor(255,128,0)
|
|
|
|
gfx.StrokeWidth(2)
|
|
|
|
gfx.Stroke()
|
|
|
|
gfx.FontSize(40)
|
|
|
|
gfx.TextAlign(gfx.TEXT_ALIGN_CENTER + gfx.TEXT_ALIGN_MIDDLE)
|
|
|
|
for i, opt in ipairs(sortingOptions) do
|
|
|
|
y = (resY/2) + (i - (sortingcursor + 1)) * 40
|
|
|
|
if selectedSorting == opt then gfx.FillColor(255,255,255) else gfx.FillColor(127,127,127) end
|
|
|
|
gfx.Text(opt, resX/2, y)
|
|
|
|
end
|
|
|
|
gfx.Restore()
|
2022-08-11 03:12:58 +02:00
|
|
|
end
|