mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 03:35:56 +02:00
importer for skins now w file picker
This commit is contained in:
+73
-33
@@ -2017,6 +2017,23 @@ local function buildSkinsPanel(imp, x, y, w, availH, m)
|
||||
local gap = m.gap
|
||||
local cy = y
|
||||
|
||||
local title = Strings("Skins/Borders")
|
||||
local bh = m.btnH
|
||||
local importLabel = imp:_skinsImportButtonLabel()
|
||||
local importW = Kit.textWidth("small", importLabel) + math.floor(24 * m.s)
|
||||
if Kit.textWidth("button", title) + importW + math.floor(24 * m.s) > w then
|
||||
importLabel = Strings("Import")
|
||||
importW = Kit.textWidth("small", importLabel) + math.floor(20 * m.s)
|
||||
end
|
||||
local place = Layout.rightCluster(x, w, math.floor(6 * m.s))
|
||||
btn(imp, place(importW), cy, importW, bh, "skins-import", importLabel, {
|
||||
kind = "accent", font = "small",
|
||||
action = function() imp:chooseSkin() end })
|
||||
Kit.text("button", Kit.ellipsize("button", title,
|
||||
math.max(0, w - importW - math.floor(12 * m.s))), x,
|
||||
cy + math.floor((bh - Kit.textHeight("button")) / 2), PAL.heading)
|
||||
cy = cy + bh + math.floor(8 * m.s)
|
||||
|
||||
if imp._skinNotice then
|
||||
cy = cy + Kit.textWrapped("small", imp._skinNotice.text, x, cy, w,
|
||||
imp._skinNotice.ok and PAL.green or PAL.red, 2) + math.floor(8 * m.s)
|
||||
@@ -2075,30 +2092,57 @@ local function buildSkinsPanel(imp, x, y, w, availH, m)
|
||||
cy = cy + rowH + math.floor(4 * m.s)
|
||||
end
|
||||
|
||||
skinRow("skin-none", nil, Strings("Built-in pad"),
|
||||
Strings("The default on-screen buttons."), active == nil,
|
||||
imp.onEditTouchControls and function()
|
||||
imp.onEditTouchControls(imp.modScope or "red")
|
||||
end or nil)
|
||||
local entries = { false }
|
||||
for _, entry in ipairs(skins) do entries[#entries + 1] = entry end
|
||||
|
||||
for _, entry in ipairs(skins) do
|
||||
local bits = {}
|
||||
bits[#bits + 1] = entry.source == "user" and Strings("installed")
|
||||
or Strings("bundled")
|
||||
if entry.controls > 0 then
|
||||
bits[#bits + 1] = entry.controls .. " " .. Strings("buttons")
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
local hint = Strings(
|
||||
"You can also drop a skin .zip on this window, or put a folder in %s/ of your save directory. RetroArch overlay .cfg files work as-is.",
|
||||
TouchSkin.USER_ROOT)
|
||||
local hintH = Kit.wrapHeight("small", hint, w, 3)
|
||||
local importH = math.floor(10 * m.s) + hintH
|
||||
|
||||
local rowGap = math.floor(4 * m.s)
|
||||
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||
local listTop = cy
|
||||
local listH = availH - (cy - y) - importH
|
||||
local perPage = Kit.rowsThatFit(listH, rowH, rowGap, 1, 20)
|
||||
if #entries > perPage then
|
||||
perPage = Kit.rowsThatFit(listH - pagerH - gap, rowH, rowGap, 1, 20)
|
||||
end
|
||||
local first, last, cur, pages = Kit.pageBounds(page(imp, "skins"),
|
||||
#entries, perPage)
|
||||
setPage(imp, "skins", cur)
|
||||
setPage(imp, "skins",
|
||||
Kit.wheelPage(x, listTop, w, listH, cur, #entries, perPage))
|
||||
|
||||
for i = first, last do
|
||||
local entry = entries[i]
|
||||
if not entry then
|
||||
skinRow("skin-none", nil, Strings("Built-in pad"),
|
||||
Strings("The default on-screen buttons."), active == nil,
|
||||
imp.onEditTouchControls and function()
|
||||
imp.onEditTouchControls(imp.modScope or "red")
|
||||
end or nil)
|
||||
else
|
||||
bits[#bits + 1] = Strings("bezel only")
|
||||
local bits = {}
|
||||
bits[#bits + 1] = entry.source == "user" and Strings("installed")
|
||||
or Strings("bundled")
|
||||
if entry.controls > 0 then
|
||||
bits[#bits + 1] = entry.controls .. " " .. Strings("buttons")
|
||||
else
|
||||
bits[#bits + 1] = Strings("bezel only")
|
||||
end
|
||||
if entry.pages > 1 then
|
||||
bits[#bits + 1] = entry.pages .. " " .. Strings("pages")
|
||||
end
|
||||
if entry.screen then bits[#bits + 1] = Strings("screen cutout") end
|
||||
local configure = imp.onOpenSkinStudio and function()
|
||||
imp.onOpenSkinStudio(imp.modScope or "red", entry.id)
|
||||
end or nil
|
||||
skinRow("skin-" .. entry.id, entry.id, entry.id,
|
||||
table.concat(bits, " \194\183 "), active == entry.id, configure)
|
||||
end
|
||||
if entry.pages > 1 then
|
||||
bits[#bits + 1] = entry.pages .. " " .. Strings("pages")
|
||||
end
|
||||
if entry.screen then bits[#bits + 1] = Strings("screen cutout") end
|
||||
local configure = imp.onOpenSkinStudio and function()
|
||||
imp.onOpenSkinStudio(imp.modScope or "red", entry.id)
|
||||
end or nil
|
||||
skinRow("skin-" .. entry.id, entry.id, entry.id,
|
||||
table.concat(bits, " \194\183 "), active == entry.id, configure)
|
||||
end
|
||||
|
||||
if #skins == 0 then
|
||||
@@ -2107,18 +2151,14 @@ local function buildSkinsPanel(imp, x, y, w, availH, m)
|
||||
cy = cy + math.floor(72 * m.s) + gap
|
||||
end
|
||||
|
||||
cy = cy + math.floor(6 * m.s)
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
Kit.caption(x, cy, Strings("IMPORT"))
|
||||
cy = cy + Kit.textHeight("small") + math.floor(6 * m.s)
|
||||
local boxH = math.floor(76 * m.s)
|
||||
Kit.card(x, cy, w, boxH, "muted")
|
||||
Kit.textWrapped("small", Strings(
|
||||
"Drop a skin .zip on this window to install it, or put a folder in the skins folder of your save directory. RetroArch overlay .cfg files work as-is."),
|
||||
x + math.floor(14 * m.s), cy + math.floor(12 * m.s),
|
||||
w - math.floor(28 * m.s), PAL.muted, 3)
|
||||
Kit.text("small", TouchSkin.USER_ROOT .. "/", x + math.floor(14 * m.s),
|
||||
cy + boxH - Kit.textHeight("small") - math.floor(10 * m.s), PAL.faint)
|
||||
if pages > 1 then
|
||||
setPage(imp, "skins",
|
||||
Kit.pager(x, cy, w, cur, #entries, perPage, "skins"))
|
||||
cy = cy + pagerH + gap
|
||||
end
|
||||
|
||||
cy = cy + math.floor(10 * m.s)
|
||||
Kit.textWrapped("small", hint, x, cy, w, PAL.muted, 3)
|
||||
end
|
||||
|
||||
local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
|
||||
+116
-5
@@ -1089,6 +1089,39 @@ local function chooseZip()
|
||||
return nil
|
||||
end
|
||||
|
||||
local function chooseSkinZip()
|
||||
local prompt = shellSafe(Strings("Choose a skin .zip"))
|
||||
local platform = love.system.getOS()
|
||||
if platform == "OS X" then
|
||||
return commandOutput(
|
||||
([[osascript -e 'POSIX path of (choose file with prompt "%s" of type {"zip"})' 2>/dev/null]])
|
||||
:format(prompt))
|
||||
elseif platform == "Windows" then
|
||||
local script = table.concat({
|
||||
"Add-Type -AssemblyName System.Windows.Forms;",
|
||||
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
|
||||
"$d.Title='" .. prompt .. "';",
|
||||
"$d.Filter='Skin archive (*.zip)|*.zip|All files (*.*)|*.*';",
|
||||
"if($d.ShowDialog() -eq 'OK'){",
|
||||
"$n=[IO.Path]::GetFileName($d.FileName) -replace '[^\\x20-\\x7E]','_';",
|
||||
"$t=Join-Path $env:TEMP $n;",
|
||||
"Copy-Item -LiteralPath $d.FileName -Destination $t -Force;",
|
||||
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
|
||||
"[Console]::Write($t)}",
|
||||
})
|
||||
return commandOutput(
|
||||
'powershell -NoProfile -STA -Command "' .. script .. '"')
|
||||
elseif platform == "Linux" then
|
||||
local path = commandOutput(
|
||||
([[zenity --file-selection --title="%s" --file-filter="Skin archive | *.zip" 2>/dev/null]])
|
||||
:format(prompt))
|
||||
if path then return path end
|
||||
return commandOutput(
|
||||
[[kdialog --getopenfilename "$HOME" "*.zip|Skin archive" 2>/dev/null]])
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Open a native picker for a raw .sav battery save (mirrors chooseZip's per-OS
|
||||
-- dialogs). Returns the chosen absolute path or nil. Android uses
|
||||
-- love.system.pickFile("sav") instead -- see RomImporter:chooseSaveImport.
|
||||
@@ -1478,7 +1511,12 @@ function RomImporter:focus(f)
|
||||
self.pickerPendingModId = nil
|
||||
self.pickerPendingImportId = nil
|
||||
elseif pickError:find("picked_mod", 1, true) then
|
||||
self.modNotice = { ok = false, text = text }
|
||||
if self.pickerPendingKind == "skin" then
|
||||
self.pickerPendingKind = nil
|
||||
self._skinNotice = { ok = false, text = text }
|
||||
else
|
||||
self.modNotice = { ok = false, text = text }
|
||||
end
|
||||
elseif pickError:find("picked_save", 1, true) then
|
||||
local version = self.androidPendingVersion or self:_savedropTarget()
|
||||
self.androidPendingVersion = nil
|
||||
@@ -1504,6 +1542,13 @@ function RomImporter:focus(f)
|
||||
end
|
||||
local modName = findPendingMod(false, self.pickSkip)
|
||||
if modName then
|
||||
if self.pickerPendingKind == "skin" then
|
||||
self.pickerPendingKind = nil
|
||||
self:_installSkinZip(modName)
|
||||
consumePick(self, modName, "picked_mod.zip",
|
||||
self._skinNotice and self._skinNotice.ok)
|
||||
return
|
||||
end
|
||||
self:_installMod(modName)
|
||||
consumePick(self, modName, "picked_mod.zip",
|
||||
self.modNotice and self.modNotice.ok)
|
||||
@@ -2498,6 +2543,11 @@ function RomImporter:update(dt)
|
||||
if Platform.isUWP() and self.modNotice and self.modNotice.ok then
|
||||
os.remove(path)
|
||||
end
|
||||
elseif kind == "skin" then
|
||||
self:_installSkinZip(path)
|
||||
if Platform.isUWP() and self._skinNotice and self._skinNotice.ok then
|
||||
os.remove(path)
|
||||
end
|
||||
elseif kind == "sav" then
|
||||
local target = version or self:_savedropTarget()
|
||||
self:_importSave(target, path)
|
||||
@@ -2520,6 +2570,8 @@ function RomImporter:update(dt)
|
||||
self.pickerPendingModId, self.pickerPendingImportId = nil, nil
|
||||
elseif kind == "mod" then
|
||||
self.modNotice = { ok = false, text = errorText }
|
||||
elseif kind == "skin" then
|
||||
self._skinNotice = { ok = false, text = errorText }
|
||||
elseif kind == "sav" then
|
||||
self.saveNotice[version] = { ok = false, text = errorText }
|
||||
else
|
||||
@@ -3047,13 +3099,27 @@ function RomImporter:_useSkin(id)
|
||||
}
|
||||
end
|
||||
|
||||
function RomImporter:_installSkinZip(file)
|
||||
function RomImporter:_installSkinZip(source)
|
||||
if self.workState == "working" then return end
|
||||
self.tab = "skins"
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
local name = file:getFilename() or ""
|
||||
local data, readError = readDroppedFile(file)
|
||||
local name, data, readError
|
||||
if type(source) == "string" then
|
||||
name = source
|
||||
if not source:match("^/") and not source:match("^%a:[/\\]")
|
||||
and not source:match("^[Ss][Dd][Mm][Cc]:") then
|
||||
data = love.filesystem.read(source)
|
||||
end
|
||||
if not data then data, readError = readExternalPath(source) end
|
||||
if not data then data = love.filesystem.read(source) end
|
||||
else
|
||||
name = source:getFilename() or ""
|
||||
data, readError = readDroppedFile(source)
|
||||
end
|
||||
if not data then
|
||||
self._skinNotice = { ok = false,
|
||||
text = "Could not read the dropped file: " .. tostring(readError) }
|
||||
text = "Could not read the skin archive: "
|
||||
.. tostring(readError or name) }
|
||||
return
|
||||
end
|
||||
local id, err = TouchSkin.installArchive(name, data)
|
||||
@@ -3065,6 +3131,51 @@ function RomImporter:_installSkinZip(file)
|
||||
self._skinNotice = { ok = true, text = "Imported " .. id }
|
||||
end
|
||||
|
||||
function RomImporter:_skinsImportButtonLabel()
|
||||
if self.isNX then return Strings("Scan again") end
|
||||
return Strings("Import skin .zip")
|
||||
end
|
||||
|
||||
function RomImporter:chooseSkin()
|
||||
if self.workState == "working" then return end
|
||||
if self.isNX then
|
||||
local found = #self:_ensureSkins(true)
|
||||
self._skinNotice = { ok = true, text = Strings(
|
||||
"%d skins found. Copy a skin .zip into %s/ over MTP, then scan again.",
|
||||
found, require("src.core.TouchSkin").USER_ROOT) }
|
||||
return
|
||||
end
|
||||
if self.nativePicker and love.system.getPickedFile then
|
||||
self.pickerPendingKind = "skin"
|
||||
if not pickFile("mod") then
|
||||
self.pickerPendingKind = nil
|
||||
self._skinNotice = { ok = false, text = "Could not open the file picker." }
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.android then
|
||||
local name = findPendingMod(true, self.pickSkip)
|
||||
if name then
|
||||
self:_installSkinZip(name)
|
||||
consumePick(self, name, "picked_mod.zip",
|
||||
self._skinNotice and self._skinNotice.ok)
|
||||
return
|
||||
end
|
||||
self.pickerPendingKind = "skin"
|
||||
if not pickFile("mod") then
|
||||
self.pickerPendingKind = nil
|
||||
self._skinNotice = { ok = false,
|
||||
text = "Could not open the file picker. Copy a skin .zip via USB." }
|
||||
else
|
||||
self.pickPending = true
|
||||
self.pickTimer = 0
|
||||
end
|
||||
return
|
||||
end
|
||||
local path = chooseSkinZip()
|
||||
if path then self:_installSkinZip(path) end
|
||||
end
|
||||
|
||||
function RomImporter:_toggleFindSearchFocus()
|
||||
self._findSearchFocus = not self._findSearchFocus
|
||||
if self._findSearchFocus then
|
||||
|
||||
@@ -94,6 +94,21 @@ return function(game)
|
||||
imp:_useSkin(nil)
|
||||
shot("skins_tab_imported.png")
|
||||
|
||||
-- import button: the picker hands back an absolute host path, not a drop
|
||||
local picked = dir .. "/picked_skin.zip"
|
||||
local pf = io.open(picked, "wb")
|
||||
pf:write(raw)
|
||||
pf:close()
|
||||
imp:_installSkinZip(picked)
|
||||
U.log("path import notice:", imp._skinNotice.text)
|
||||
U.log("picked_skin found:", tostring(TouchSkin.find("picked_skin") ~= nil))
|
||||
U.log("import label:", imp:_skinsImportButtonLabel())
|
||||
shot("skins_tab_path_imported.png")
|
||||
|
||||
love.window.setMode(520, 760, { resizable = true, highdpi = true })
|
||||
U.wait(3)
|
||||
shot("skins_tab_narrow.png")
|
||||
|
||||
U.log("done")
|
||||
love.event.quit()
|
||||
while true do coroutine.yield() end
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
-- Skins tab Import button: picker plumbing + path/drop install (SKINIMP-01..05).
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local S = require("tests.harness").suite("skin import picker")
|
||||
local eq = S.eq
|
||||
local check = S.check
|
||||
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
local TouchSkin = require("src.core.TouchSkin")
|
||||
|
||||
love.system = love.system or {}
|
||||
local saved = {
|
||||
pickFile = love.system.pickFile,
|
||||
getPickedFile = love.system.getPickedFile,
|
||||
getPickError = love.system.getPickError,
|
||||
installArchive = TouchSkin.installArchive,
|
||||
}
|
||||
|
||||
local installed
|
||||
TouchSkin.installArchive = function(name, data)
|
||||
installed = { name = name, data = data }
|
||||
if not data or data == "" then return nil, "empty archive" end
|
||||
return (name:match("([^/\\]+)$") or name):gsub("%.[Zz][Ii][Pp]$", "")
|
||||
end
|
||||
|
||||
local function freshImporter(fields)
|
||||
local imp = RomImporter.new(function() end, { launcher = true })
|
||||
imp.tab = "skins"
|
||||
imp._skins = {}
|
||||
imp._ensureSkins = function(self) return self._skins end
|
||||
for k, v in pairs(fields or {}) do imp[k] = v end
|
||||
return imp
|
||||
end
|
||||
|
||||
-- SKINIMP-01: a picker path installs as a skin, not a mod
|
||||
installed = nil
|
||||
local imp = freshImporter()
|
||||
local tmp = os.tmpname() .. ".zip"
|
||||
local f = assert(io.open(tmp, "wb"))
|
||||
f:write("PK\3\4skin-archive")
|
||||
f:close()
|
||||
imp:_installSkinZip(tmp)
|
||||
os.remove(tmp)
|
||||
check(installed ~= nil, "an absolute picker path reaches TouchSkin.installArchive")
|
||||
eq(installed.data, "PK\3\4skin-archive", "the picked file's bytes are installed")
|
||||
check(imp._skinNotice and imp._skinNotice.ok, "a good pick reports success")
|
||||
|
||||
-- SKINIMP-02: a dropped file still installs, and both paths land on the tab
|
||||
installed = nil
|
||||
imp = freshImporter({ tab = "red" })
|
||||
imp:filedropped({
|
||||
getFilename = function() return "/tmp/dropped_skin.zip" end,
|
||||
getSize = function() return 4 end,
|
||||
open = function() return true end,
|
||||
read = function() return "PKZP" end,
|
||||
close = function() return true end,
|
||||
})
|
||||
check(installed == nil, "a zip dropped off the skins tab is still a mod")
|
||||
installed = nil
|
||||
imp = freshImporter()
|
||||
imp:filedropped({
|
||||
getFilename = function() return "/tmp/dropped_skin.zip" end,
|
||||
getSize = function() return 4 end,
|
||||
open = function() return true end,
|
||||
read = function() return "PKZP" end,
|
||||
close = function() return true end,
|
||||
})
|
||||
eq(installed and installed.name, "/tmp/dropped_skin.zip",
|
||||
"a zip dropped on the skins tab installs as a skin")
|
||||
eq(imp.tab, "skins", "the skin install stays on the skins tab")
|
||||
|
||||
-- SKINIMP-03: the mobile bridge borrows the mod picker kind
|
||||
local requestedKind
|
||||
love.system.pickFile = function(kind)
|
||||
requestedKind = kind
|
||||
return true
|
||||
end
|
||||
love.system.getPickedFile = function() return nil end
|
||||
love.system.getPickError = function() return nil end
|
||||
imp = freshImporter({ nativePicker = true })
|
||||
imp:chooseSkin()
|
||||
eq(requestedKind, "mod", "chooseSkin opens the .zip picker")
|
||||
eq(imp.pickerPendingKind, "skin", "the pending pick is routed to the skins tab")
|
||||
|
||||
-- SKINIMP-04: the picked file comes back through update() as a skin
|
||||
installed = nil
|
||||
love.system.getPickedFile = function()
|
||||
love.system.getPickedFile = function() return nil end
|
||||
return "/picked/from_bridge.zip"
|
||||
end
|
||||
imp._installSkinZip = function(self, source)
|
||||
self.installedSource = source
|
||||
self._skinNotice = { ok = true, text = "Imported from_bridge" }
|
||||
end
|
||||
imp:update(0)
|
||||
eq(imp.installedSource, "/picked/from_bridge.zip",
|
||||
"update() hands the picked path to the skin installer")
|
||||
eq(imp.pickerPendingKind, nil, "the pending kind clears once consumed")
|
||||
|
||||
-- SKINIMP-05: a picker error lands on the skins notice, not the mods one
|
||||
love.system.getPickedFile = function() return nil end
|
||||
love.system.getPickError = function()
|
||||
love.system.getPickError = function() return nil end
|
||||
return "picker refused"
|
||||
end
|
||||
imp = freshImporter({ nativePicker = true, pickerPendingKind = "skin" })
|
||||
imp:update(0)
|
||||
check(imp._skinNotice and not imp._skinNotice.ok,
|
||||
"a failed skin pick reports on the skins tab")
|
||||
eq(imp.modNotice, nil, "a failed skin pick leaves the mods notice alone")
|
||||
|
||||
love.system.pickFile = saved.pickFile
|
||||
love.system.getPickedFile = saved.getPickedFile
|
||||
love.system.getPickError = saved.getPickError
|
||||
TouchSkin.installArchive = saved.installArchive
|
||||
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user