diff --git a/mobile/ios/native/GRPickerBridge.swift b/mobile/ios/native/GRPickerBridge.swift index 3298705e..9f3e3966 100644 --- a/mobile/ios/native/GRPickerBridge.swift +++ b/mobile/ios/native/GRPickerBridge.swift @@ -30,6 +30,45 @@ public final class GRPickerBridge: NSObject { // (/Library/Application Support/). private static let loveIdentity = "pokemon-love2d" + @objc(httpDownloadWithUrl:destination:userAgent:accept:) + public static func httpDownload(url: UnsafePointer?, + destination: UnsafePointer?, + userAgent: UnsafePointer?, + accept: UnsafePointer?) -> 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:) diff --git a/mobile/ios/patch_love_src.py b/mobile/ios/patch_love_src.py index cf09295e..ef9d1b39 100644 --- a/mobile/ios/patch_love_src.py +++ b/mobile/ios/patch_love_src.py @@ -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(): diff --git a/src/core/HostShell.lua b/src/core/HostShell.lua index 32850e64..96dbd065 100644 --- a/src/core/HostShell.lua +++ b/src/core/HostShell.lua @@ -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. diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index ab4f6d01..150f15f1 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -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 {} diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index b39458b4..86ce0df2 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -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() diff --git a/tests/engine/launcher_text_input_bug578.lua b/tests/engine/launcher_text_input_bug578.lua index 222232b8..8d6061af 100644 --- a/tests/engine/launcher_text_input_bug578.lua +++ b/tests/engine/launcher_text_input_bug578.lua @@ -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