diff --git a/README.md b/README.md index 1bf5854..7a3319f 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ These settings are written to `.../Android/data//files/super3/Config/Su - The app may offer an optional "sync"/export workflow using Android's Storage Access Framework. If enabled, the app only accesses the folder(s) you explicitly choose. ## Flyers -- Flyers are installed to `.../Android/data//files/super3/Flyers/` as `_front.png` and `_back.png`. +- Flyers are downloaded on-demand (when you switch to Flyer view) from `https://github.com/izzy2lost/Model3flyers`. +- They are stored in `.../Android/data//files/super3/Flyers/` as `_front.png` and `_back.png`. - You can replace or add files in that folder to customize artwork. ## Legal / Google Play Policy Notes @@ -39,7 +40,7 @@ These settings are written to `.../Android/data//files/super3/Config/Su - **Not affiliated:** This project is not affiliated with or endorsed by SEGA or any arcade hardware manufacturer. ## Privacy -- SUPER3 is designed to run offline and does not require network access. +- SUPER3 can run offline for emulation, but Flyer view may download artwork from GitHub. - The app stores emulator settings, save states, and screenshots locally on your device. - If you opt into selecting a user folder for syncing/export, access is limited to the folder you select via the system file picker. diff --git a/Src/Model3/53C810.cpp b/Src/Model3/53C810.cpp index e9d95ad..70eed09 100644 --- a/Src/Model3/53C810.cpp +++ b/Src/Model3/53C810.cpp @@ -423,7 +423,7 @@ UINT32 C53C810::ReadPCIConfigSpace(unsigned device, unsigned reg, unsigned bits, { UINT32 d; - if ((bits==8)) + if (bits == 8) { DebugLog("53C810 %d-bit PCI read request for reg=%02X\n", bits, reg); return 0; diff --git a/Src/Model3/Real3D.cpp b/Src/Model3/Real3D.cpp index 73d8fa3..058baf1 100644 --- a/Src/Model3/Real3D.cpp +++ b/Src/Model3/Real3D.cpp @@ -823,7 +823,7 @@ uint32_t CReal3D::ReadPCIConfigSpace(unsigned device, unsigned reg, unsigned bit { uint32_t d; - if ((bits==8)) + if (bits == 8) { DebugLog("Real3D: %d-bit PCI read request for reg=%02X\n", bits, reg); return 0; diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 09db7f9..bb3946a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + Unit, + ): FlyerSyncResult { + destDir.mkdirs() + + val entries = fetchEntries() + + var done = 0 + var downloaded = 0 + var skipped = 0 + val total = entries.size + + for (e in entries) { + done += 1 + onProgress(done, total, e.name) + + val outFile = File(destDir, e.name) + if (outFile.exists() && e.size > 0L && outFile.length() == e.size) { + skipped += 1 + continue + } + + downloadToFile(e.downloadUrl, outFile) + downloaded += 1 + } + + return FlyerSyncResult(total = total, downloaded = downloaded, skipped = skipped) + } + + private data class Entry( + val name: String, + val size: Long, + val downloadUrl: String, + ) + + private fun fetchEntries(): List { + val conn = (URL(CONTENTS_URL).openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + connectTimeout = 15_000 + readTimeout = 30_000 + setRequestProperty("Accept", "application/vnd.github+json") + setRequestProperty("User-Agent", USER_AGENT) + } + + val code = runCatching { conn.responseCode }.getOrElse { -1 } + + if (code != 200) { + conn.disconnect() + throw IllegalStateException("GitHub contents request failed: HTTP $code") + } + + val body = conn.inputStream.bufferedReader().use { it.readText() } + conn.disconnect() + + val arr = JSONArray(body) + val out = ArrayList(arr.length()) + for (i in 0 until arr.length()) { + val obj = arr.getJSONObject(i) + if (obj.optString("type") != "file") continue + val name = obj.optString("name").orEmpty() + if (!name.endsWith(".png", ignoreCase = true)) continue + val base = name.substringBeforeLast('.', name) + if (!(base.endsWith("_front", ignoreCase = true) || base.endsWith("_back", ignoreCase = true))) continue + val downloadUrl = obj.optString("download_url").orEmpty() + if (downloadUrl.isBlank()) continue + val size = obj.optLong("size", 0L) + out.add(Entry(name = name, size = size, downloadUrl = downloadUrl)) + } + return out + } + + private fun downloadToFile(url: String, outFile: File) { + outFile.parentFile?.mkdirs() + val tmp = File(outFile.parentFile, outFile.name + ".download") + + val conn = (URL(url).openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + connectTimeout = 15_000 + readTimeout = 60_000 + setRequestProperty("User-Agent", USER_AGENT) + } + + val code = runCatching { conn.responseCode }.getOrElse { -1 } + if (code != 200) { + conn.disconnect() + throw IllegalStateException("Flyer download failed: HTTP $code") + } + + BufferedInputStream(conn.inputStream).use { input -> + tmp.outputStream().use { output -> + input.copyTo(output) + } + } + conn.disconnect() + + if (outFile.exists()) outFile.delete() + if (!tmp.renameTo(outFile)) { + tmp.copyTo(outFile, overwrite = true) + tmp.delete() + } + } +} diff --git a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt index 713e80e..b32b379 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt @@ -65,6 +65,12 @@ class MainActivity : AppCompatActivity() { @Volatile private var scanning = false + @Volatile + private var syncingFlyers = false + + @Volatile + private var flyersStatusToken: Long = 0L + private data class VideoSettings( val xResolution: Int, val yResolution: Int, @@ -257,6 +263,10 @@ class MainActivity : AppCompatActivity() { private fun applyViewMode(mode: GameListViewMode) { gamesAdapter.setViewMode(mode) + if (mode == GameListViewMode.FLYERS) { + ensureFlyersSynced() + } + val lm = when (mode) { GameListViewMode.LIST -> androidx.recyclerview.widget.LinearLayoutManager(this) @@ -285,6 +295,58 @@ class MainActivity : AppCompatActivity() { gamesAdapter.submitList(buildItems(games, zipDocs, flyersDir())) } + private fun ensureFlyersSynced() { + if (syncingFlyers) return + + val dir = flyersDir() + val last = prefs.getLong("flyers_sync_last_ms", 0L) + val now = System.currentTimeMillis() + val hasAny = dir.exists() && (dir.listFiles()?.any { it.isFile } == true) + val minIntervalMs = 6L * 60L * 60L * 1000L + if (hasAny && now - last < minIntervalMs) return + + syncingFlyers = true + flyersStatusToken = now + Toast.makeText(this, "Syncing flyers…", Toast.LENGTH_SHORT).show() + + val priorStatus = statusText.text?.toString().orEmpty() + statusText.text = "Downloading flyers…" + + thread(name = "Super3FlyerSync") { + val result = + runCatching { + FlyerRepoSync.syncInto(dir) { done, total, name -> + runOnUiThread { + if (flyersStatusToken == now) { + statusText.text = "Downloading flyers… ($done/$total) $name" + } + } + } + } + + runOnUiThread { + syncingFlyers = false + if (flyersStatusToken == now) { + statusText.text = priorStatus + flyersStatusToken = 0L + } + + val ok = result.getOrNull() + if (ok == null) { + Toast.makeText(this, "Flyer download failed", Toast.LENGTH_SHORT).show() + return@runOnUiThread + } + + val sync = ok + prefs.edit().putLong("flyers_sync_last_ms", now).apply() + + if (sync.downloaded > 0) { + gamesAdapter.submitList(buildItems(games, zipDocs, dir)) + } + } + } + } + private fun loadViewMode(): GameListViewMode { return when (prefs.getString("ui_view_mode", null)) { "list" -> GameListViewMode.LIST