Merge pull request #755 from castdrian/ios

fix(ios): support mod index networking and text input dismissal
This commit is contained in:
bryanthaboi
2026-08-03 16:11:27 -04:00
committed by GitHub
6 changed files with 90 additions and 9 deletions
+39
View File
@@ -30,6 +30,45 @@ public final class GRPickerBridge: NSObject {
// (<sandbox>/Library/Application Support/<identity>).
private static let loveIdentity = "pokemon-love2d"
@objc(httpDownloadWithUrl:destination:userAgent:accept:)
public static func httpDownload(url: UnsafePointer<CChar>?,
destination: UnsafePointer<CChar>?,
userAgent: UnsafePointer<CChar>?,
accept: UnsafePointer<CChar>?) -> Bool {
guard let url, let destination,
let requestURL = URL(string: String(cString: url)) else { return false }
var request = URLRequest(url: requestURL)
request.timeoutInterval = 300
if let userAgent, userAgent.pointee != 0 {
request.setValue(String(cString: userAgent), forHTTPHeaderField: "User-Agent")
}
if let accept, accept.pointee != 0 {
request.setValue(String(cString: accept), forHTTPHeaderField: "Accept")
}
let target = URL(fileURLWithPath: String(cString: destination))
let semaphore = DispatchSemaphore(value: 0)
var succeeded = false
let task = URLSession.shared.downloadTask(with: request) { temporary, response, error in
defer { semaphore.signal() }
guard error == nil, let temporary,
let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode) else { return }
try? FileManager.default.removeItem(at: target)
do {
try FileManager.default.moveItem(at: temporary, to: target)
succeeded = true
} catch {
succeeded = false
}
}
task.resume()
guard semaphore.wait(timeout: .now() + 305) == .success else {
task.cancel()
return false
}
return succeeded
}
// MARK: - Entry points called from liblove (C strings on purpose)
@objc(presentPickerWithKind:saveDir:)
+30 -2
View File
@@ -103,6 +103,7 @@ WRAP_REGISTRATION = """#ifdef LOVE_IOS
{ "pickFile", w_pickFile },
{ "createFile", w_createFile },
{ "syncHealthSteps", w_syncHealthSteps },
{ "httpDownload", w_httpDownload },
#endif
"""
@@ -144,6 +145,32 @@ int w_syncHealthSteps(lua_State *L)
WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS
{ "syncHealthSteps", w_syncHealthSteps },
{ "httpDownload", w_httpDownload },
#endif
"""
BRIDGE_EXTRA_FUNCS = """
#ifdef LOVE_IOS
int w_httpDownload(lua_State *L)
{
const char *url = luaL_checkstring(L, 1);
const char *destination = luaL_checkstring(L, 2);
const char *userAgent = luaL_optstring(L, 3, "gen1recomp");
const char *accept = luaL_optstring(L, 4, "");
Class cls = objc_getClass("GRPickerBridge");
if (cls == nullptr)
{
lua_pushboolean(L, 0);
return 1;
}
typedef signed char (*GRDownload)(Class, SEL, const char *, const char *,
const char *, const char *);
signed char ok = ((GRDownload)objc_msgSend)(
cls, sel_registerName("httpDownloadWithUrl:destination:userAgent:accept:"),
url, destination, userAgent, accept);
lua_pushboolean(L, ok != 0);
return 1;
}
#endif
"""
@@ -216,7 +243,8 @@ def patch_wrap_system():
if anchor not in text:
fail(f"anchor not found in {WRAP_SYSTEM}")
has_native_picker = re.search(r"\bint w_pickFile\s*\(", text) is not None
text = text.replace(anchor, (WRAP_SYNC_FUNCS if has_native_picker else WRAP_FUNCS) + anchor, 1)
bridge_funcs = WRAP_SYNC_FUNCS if has_native_picker else WRAP_FUNCS
text = text.replace(anchor, bridge_funcs + BRIDGE_EXTRA_FUNCS + anchor, 1)
reg_anchor = '\t{ "vibrate", w_vibrate },\n'
if reg_anchor not in text:
fail(f"registration anchor not found in {WRAP_SYSTEM}")
@@ -224,7 +252,7 @@ def patch_wrap_system():
text = text.replace(reg_anchor, reg_anchor + registration, 1)
WRAP_SYSTEM.write_text(text)
print("patch_love_src: wrap_System.cpp patched "
"(pickFile/createFile/syncHealthSteps)")
"(pickFile/createFile/syncHealthSteps/httpDownload)")
def patch_pbxproj():
+4 -5
View File
@@ -148,15 +148,14 @@ function HostShell.haveCurl()
return readOk and out ~= nil and out:find("curl", 1, true) ~= nil
end
-- The bridge only exists in our Android liblove. An older APK reports nil
-- here and falls back to the "no transport" error the callers already show;
-- the iOS build compiles the same wrapper but always returns false, so gate
-- on the OS as well and keep its error message honest.
-- An older mobile build reports nil here and falls back to the "no transport"
-- error the callers already show.
local function haveBridge()
if not (love and love.system and type(love.system.httpDownload) == "function") then
return false
end
return love.system.getOS and love.system.getOS() == "Android"
local osName = love.system.getOS and love.system.getOS()
return osName == "Android" or osName == "iOS"
end
-- Is any transport available at all? Callers gate on this, never on curl.
+1 -2
View File
@@ -1214,8 +1214,7 @@ local function buildFindPanel(imp, parent, m)
imp.findQuery or "", Strings("Search mods"),
imp._findSearchFocus == true,
function()
imp._findSearchFocus = true
imp:_armTextInput()
imp:_toggleFindSearchFocus()
end)
local cats = (imp.findIndex and imp.findIndex.categories) or {}
+9
View File
@@ -1733,6 +1733,15 @@ function RomImporter:_switchTab(id)
self:_disarmTextInput()
end
function RomImporter:_toggleFindSearchFocus()
self._findSearchFocus = not self._findSearchFocus
if self._findSearchFocus then
self:_armTextInput()
else
self:_disarmTextInput()
end
end
-- ------- settings gear (options.lua + enabled mods' option schemas)
function RomImporter:_openSettings()
@@ -133,6 +133,13 @@ check(ri._findSearchFocus == false, "a tab change drops the caret")
eq(lastArm(), false, "and disarms setTextInput")
ri.tab = "find"
ri:_toggleFindSearchFocus()
check(ri._findSearchFocus == true, "tapping the search field focuses it")
eq(lastArm(), true, "refocusing the search field arms setTextInput")
ri:_toggleFindSearchFocus()
check(ri._findSearchFocus == false, "tapping the focused search field blurs it")
eq(lastArm(), false, "blurring the search field disarms setTextInput")
-- ---- desktop contract (#529): disarm never lowers off Android -------------
ri.android = false