diff --git a/README.md b/README.md index 7f875f0..1bf5854 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This app does **not** include any games, ROMs, BIOS files, or copyrighted game c - Virtual on-screen steering wheel for driving games (when supported by the title). - Save states (10 slots) with automatic screenshot thumbnails and tap-to-preview. - In-game pause/resume button. +- Game flyers (front/back) in the launcher list. ## Video settings (Resolution & Widescreen) From the main game list, open the side menu (toolbar "menu" icon) and look under **Video**. @@ -27,6 +28,10 @@ These settings are written to `.../Android/data//files/super3/Config/Su - Save states and thumbnails are stored under the app's user data directory in `Saves/` (for example: `.../Android/data//files/super3/Saves/`). - 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`. +- You can replace or add files in that folder to customize artwork. + ## Legal / Google Play Policy Notes - **No games included:** SUPER3 does not ship with ROMs or game data, and it is not intended to facilitate piracy. - **You must own the games:** Only use ROMs you are legally permitted to use in your region. @@ -34,7 +39,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 does not require the `INTERNET` permission and is designed to run offline. +- SUPER3 is designed to run offline and does not require network access. - 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/Graphics/New3D/New3D.cpp b/Src/Graphics/New3D/New3D.cpp index 8542768..1757c2e 100644 --- a/Src/Graphics/New3D/New3D.cpp +++ b/Src/Graphics/New3D/New3D.cpp @@ -3,6 +3,7 @@ #include "Vec.h" #include #include +#include #include #include #include @@ -284,6 +285,40 @@ bool CNew3D::SkipLayer(int layer) return true; } +namespace { +struct FVertexOffsets +{ + std::size_t pos; + std::size_t normal; + std::size_t texcoords; + std::size_t fixedShade; + std::size_t faceColour; + std::size_t faceNormal; +}; + +static void* VboOffset(std::size_t offset) +{ + return reinterpret_cast(static_cast(offset)); +} + +static const FVertexOffsets& GetFVertexOffsets() +{ + static const FVertexOffsets offsets = [] { + FVertex v {}; + const auto* base = reinterpret_cast(&v); + return FVertexOffsets { + static_cast(reinterpret_cast(v.pos) - base), + static_cast(reinterpret_cast(v.normal) - base), + static_cast(reinterpret_cast(v.texcoords) - base), + static_cast(reinterpret_cast(&v.fixedShade) - base), + static_cast(reinterpret_cast(v.faceColour) - base), + static_cast(reinterpret_cast(v.faceNormal) - base), + }; + }(); + return offsets; +} +} // namespace + void CNew3D::SetRenderStates() { m_vbo.Bind(true); @@ -297,12 +332,13 @@ void CNew3D::SetRenderStates() glEnableVertexAttribArray(5); // before draw, specify vertex and index arrays with their offsets, offsetof is maybe evil .. - glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inVertex"), 4, GL_FLOAT, GL_FALSE, sizeof(FVertex), 0); - glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inNormal"), 3, GL_FLOAT, GL_FALSE, sizeof(FVertex), (void*)offsetof(FVertex, normal)); - glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inTexCoord"), 2, GL_FLOAT, GL_FALSE, sizeof(FVertex), (void*)offsetof(FVertex, texcoords)); - glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inColour"), 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(FVertex), (void*)offsetof(FVertex, faceColour)); - glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inFaceNormal"), 3, GL_FLOAT, GL_FALSE, sizeof(FVertex), (void*)offsetof(FVertex, faceNormal)); - glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inFixedShade"), 1, GL_FLOAT, GL_FALSE, sizeof(FVertex), (void*)offsetof(FVertex, fixedShade)); + const auto& offsets = GetFVertexOffsets(); + glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inVertex"), 4, GL_FLOAT, GL_FALSE, sizeof(FVertex), VboOffset(offsets.pos)); + glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inNormal"), 3, GL_FLOAT, GL_FALSE, sizeof(FVertex), VboOffset(offsets.normal)); + glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inTexCoord"), 2, GL_FLOAT, GL_FALSE, sizeof(FVertex), VboOffset(offsets.texcoords)); + glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inColour"), 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(FVertex), VboOffset(offsets.faceColour)); + glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inFaceNormal"), 3, GL_FLOAT, GL_FALSE, sizeof(FVertex), VboOffset(offsets.faceNormal)); + glVertexAttribPointer(m_r3dShader.GetVertexAttribPos("inFixedShade"), 1, GL_FLOAT, GL_FALSE, sizeof(FVertex), VboOffset(offsets.fixedShade)); glDepthFunc (GL_LEQUAL); glEnable (GL_DEPTH_TEST); diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a11936c..3e1e8b4 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -62,6 +62,7 @@ android { dependencies { implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.documentfile:documentfile:1.0.1") implementation("androidx.recyclerview:recyclerview:1.4.0") implementation("com.google.android.material:material:1.14.0-alpha07") diff --git a/android/app/src/main/assets/Flyers/bassdx_back.png b/android/app/src/main/assets/Flyers/bassdx_back.png new file mode 100644 index 0000000..7650ad8 Binary files /dev/null and b/android/app/src/main/assets/Flyers/bassdx_back.png differ diff --git a/android/app/src/main/assets/Flyers/bassdx_front.png b/android/app/src/main/assets/Flyers/bassdx_front.png new file mode 100644 index 0000000..a23c078 Binary files /dev/null and b/android/app/src/main/assets/Flyers/bassdx_front.png differ diff --git a/android/app/src/main/assets/Flyers/dayto2pe_back.png b/android/app/src/main/assets/Flyers/dayto2pe_back.png new file mode 100644 index 0000000..0da6b90 Binary files /dev/null and b/android/app/src/main/assets/Flyers/dayto2pe_back.png differ diff --git a/android/app/src/main/assets/Flyers/dayto2pe_front.png b/android/app/src/main/assets/Flyers/dayto2pe_front.png new file mode 100644 index 0000000..53525de Binary files /dev/null and b/android/app/src/main/assets/Flyers/dayto2pe_front.png differ diff --git a/android/app/src/main/assets/Flyers/daytona2_back.png b/android/app/src/main/assets/Flyers/daytona2_back.png new file mode 100644 index 0000000..60e2113 Binary files /dev/null and b/android/app/src/main/assets/Flyers/daytona2_back.png differ diff --git a/android/app/src/main/assets/Flyers/daytona2_front.png b/android/app/src/main/assets/Flyers/daytona2_front.png new file mode 100644 index 0000000..15869b0 Binary files /dev/null and b/android/app/src/main/assets/Flyers/daytona2_front.png differ diff --git a/android/app/src/main/assets/Flyers/dirtdvls_back.png b/android/app/src/main/assets/Flyers/dirtdvls_back.png new file mode 100644 index 0000000..742d1c8 Binary files /dev/null and b/android/app/src/main/assets/Flyers/dirtdvls_back.png differ diff --git a/android/app/src/main/assets/Flyers/dirtdvls_front.png b/android/app/src/main/assets/Flyers/dirtdvls_front.png new file mode 100644 index 0000000..be585a0 Binary files /dev/null and b/android/app/src/main/assets/Flyers/dirtdvls_front.png differ diff --git a/android/app/src/main/assets/Flyers/dirtdvlsj_back.png b/android/app/src/main/assets/Flyers/dirtdvlsj_back.png new file mode 100644 index 0000000..ea1b665 Binary files /dev/null and b/android/app/src/main/assets/Flyers/dirtdvlsj_back.png differ diff --git a/android/app/src/main/assets/Flyers/dirtdvlsj_front.png b/android/app/src/main/assets/Flyers/dirtdvlsj_front.png new file mode 100644 index 0000000..5dc3e0d Binary files /dev/null and b/android/app/src/main/assets/Flyers/dirtdvlsj_front.png differ diff --git a/android/app/src/main/assets/Flyers/eca_back.png b/android/app/src/main/assets/Flyers/eca_back.png new file mode 100644 index 0000000..7d37980 Binary files /dev/null and b/android/app/src/main/assets/Flyers/eca_back.png differ diff --git a/android/app/src/main/assets/Flyers/eca_front.png b/android/app/src/main/assets/Flyers/eca_front.png new file mode 100644 index 0000000..d038cac Binary files /dev/null and b/android/app/src/main/assets/Flyers/eca_front.png differ diff --git a/android/app/src/main/assets/Flyers/fvipers2_back.png b/android/app/src/main/assets/Flyers/fvipers2_back.png new file mode 100644 index 0000000..09685fe Binary files /dev/null and b/android/app/src/main/assets/Flyers/fvipers2_back.png differ diff --git a/android/app/src/main/assets/Flyers/fvipers2_front.png b/android/app/src/main/assets/Flyers/fvipers2_front.png new file mode 100644 index 0000000..41eb46e Binary files /dev/null and b/android/app/src/main/assets/Flyers/fvipers2_front.png differ diff --git a/android/app/src/main/assets/Flyers/harley_back.png b/android/app/src/main/assets/Flyers/harley_back.png new file mode 100644 index 0000000..e3876a0 Binary files /dev/null and b/android/app/src/main/assets/Flyers/harley_back.png differ diff --git a/android/app/src/main/assets/Flyers/harley_front.png b/android/app/src/main/assets/Flyers/harley_front.png new file mode 100644 index 0000000..3248962 Binary files /dev/null and b/android/app/src/main/assets/Flyers/harley_front.png differ diff --git a/android/app/src/main/assets/Flyers/lamachin_back.png b/android/app/src/main/assets/Flyers/lamachin_back.png new file mode 100644 index 0000000..b6dad7d Binary files /dev/null and b/android/app/src/main/assets/Flyers/lamachin_back.png differ diff --git a/android/app/src/main/assets/Flyers/lamachin_front.png b/android/app/src/main/assets/Flyers/lamachin_front.png new file mode 100644 index 0000000..3116a75 Binary files /dev/null and b/android/app/src/main/assets/Flyers/lamachin_front.png differ diff --git a/android/app/src/main/assets/Flyers/lemans24_back.png b/android/app/src/main/assets/Flyers/lemans24_back.png new file mode 100644 index 0000000..21abe27 Binary files /dev/null and b/android/app/src/main/assets/Flyers/lemans24_back.png differ diff --git a/android/app/src/main/assets/Flyers/lemans24_front.png b/android/app/src/main/assets/Flyers/lemans24_front.png new file mode 100644 index 0000000..2a37aa2 Binary files /dev/null and b/android/app/src/main/assets/Flyers/lemans24_front.png differ diff --git a/android/app/src/main/assets/Flyers/lostwsga_back.png b/android/app/src/main/assets/Flyers/lostwsga_back.png new file mode 100644 index 0000000..03af29f Binary files /dev/null and b/android/app/src/main/assets/Flyers/lostwsga_back.png differ diff --git a/android/app/src/main/assets/Flyers/lostwsga_front.png b/android/app/src/main/assets/Flyers/lostwsga_front.png new file mode 100644 index 0000000..b5f4019 Binary files /dev/null and b/android/app/src/main/assets/Flyers/lostwsga_front.png differ diff --git a/android/app/src/main/assets/Flyers/magtruck_back.png b/android/app/src/main/assets/Flyers/magtruck_back.png new file mode 100644 index 0000000..be24b7d Binary files /dev/null and b/android/app/src/main/assets/Flyers/magtruck_back.png differ diff --git a/android/app/src/main/assets/Flyers/magtruck_front.png b/android/app/src/main/assets/Flyers/magtruck_front.png new file mode 100644 index 0000000..54b59f5 Binary files /dev/null and b/android/app/src/main/assets/Flyers/magtruck_front.png differ diff --git a/android/app/src/main/assets/Flyers/oceanhun_back.png b/android/app/src/main/assets/Flyers/oceanhun_back.png new file mode 100644 index 0000000..04b8236 Binary files /dev/null and b/android/app/src/main/assets/Flyers/oceanhun_back.png differ diff --git a/android/app/src/main/assets/Flyers/oceanhun_front.png b/android/app/src/main/assets/Flyers/oceanhun_front.png new file mode 100644 index 0000000..7caba5c Binary files /dev/null and b/android/app/src/main/assets/Flyers/oceanhun_front.png differ diff --git a/android/app/src/main/assets/Flyers/scud_back.png b/android/app/src/main/assets/Flyers/scud_back.png new file mode 100644 index 0000000..f4fa779 Binary files /dev/null and b/android/app/src/main/assets/Flyers/scud_back.png differ diff --git a/android/app/src/main/assets/Flyers/scud_front.png b/android/app/src/main/assets/Flyers/scud_front.png new file mode 100644 index 0000000..d97fe2e Binary files /dev/null and b/android/app/src/main/assets/Flyers/scud_front.png differ diff --git a/android/app/src/main/assets/Flyers/skichamp_back.png b/android/app/src/main/assets/Flyers/skichamp_back.png new file mode 100644 index 0000000..3583692 Binary files /dev/null and b/android/app/src/main/assets/Flyers/skichamp_back.png differ diff --git a/android/app/src/main/assets/Flyers/skichamp_front.png b/android/app/src/main/assets/Flyers/skichamp_front.png new file mode 100644 index 0000000..b1f9f0b Binary files /dev/null and b/android/app/src/main/assets/Flyers/skichamp_front.png differ diff --git a/android/app/src/main/assets/Flyers/spikeofe_back.png b/android/app/src/main/assets/Flyers/spikeofe_back.png new file mode 100644 index 0000000..af0d41d Binary files /dev/null and b/android/app/src/main/assets/Flyers/spikeofe_back.png differ diff --git a/android/app/src/main/assets/Flyers/spikeofe_front.png b/android/app/src/main/assets/Flyers/spikeofe_front.png new file mode 100644 index 0000000..513db6b Binary files /dev/null and b/android/app/src/main/assets/Flyers/spikeofe_front.png differ diff --git a/android/app/src/main/assets/Flyers/spikeout_back.png b/android/app/src/main/assets/Flyers/spikeout_back.png new file mode 100644 index 0000000..2694bd2 Binary files /dev/null and b/android/app/src/main/assets/Flyers/spikeout_back.png differ diff --git a/android/app/src/main/assets/Flyers/spikeout_front.png b/android/app/src/main/assets/Flyers/spikeout_front.png new file mode 100644 index 0000000..fcdd866 Binary files /dev/null and b/android/app/src/main/assets/Flyers/spikeout_front.png differ diff --git a/android/app/src/main/assets/Flyers/srally2_back.png b/android/app/src/main/assets/Flyers/srally2_back.png new file mode 100644 index 0000000..120b647 Binary files /dev/null and b/android/app/src/main/assets/Flyers/srally2_back.png differ diff --git a/android/app/src/main/assets/Flyers/srally2_front.png b/android/app/src/main/assets/Flyers/srally2_front.png new file mode 100644 index 0000000..9a1423b Binary files /dev/null and b/android/app/src/main/assets/Flyers/srally2_front.png differ diff --git a/android/app/src/main/assets/Flyers/swtrilgy_back.png b/android/app/src/main/assets/Flyers/swtrilgy_back.png new file mode 100644 index 0000000..43c035c Binary files /dev/null and b/android/app/src/main/assets/Flyers/swtrilgy_back.png differ diff --git a/android/app/src/main/assets/Flyers/swtrilgy_front.png b/android/app/src/main/assets/Flyers/swtrilgy_front.png new file mode 100644 index 0000000..277242e Binary files /dev/null and b/android/app/src/main/assets/Flyers/swtrilgy_front.png differ diff --git a/android/app/src/main/assets/Flyers/vf3_back.png b/android/app/src/main/assets/Flyers/vf3_back.png new file mode 100644 index 0000000..b4d4f3e Binary files /dev/null and b/android/app/src/main/assets/Flyers/vf3_back.png differ diff --git a/android/app/src/main/assets/Flyers/vf3_front.png b/android/app/src/main/assets/Flyers/vf3_front.png new file mode 100644 index 0000000..40e2b7d Binary files /dev/null and b/android/app/src/main/assets/Flyers/vf3_front.png differ diff --git a/android/app/src/main/assets/Flyers/von2_back.png b/android/app/src/main/assets/Flyers/von2_back.png new file mode 100644 index 0000000..0008ccf Binary files /dev/null and b/android/app/src/main/assets/Flyers/von2_back.png differ diff --git a/android/app/src/main/assets/Flyers/von2_front.png b/android/app/src/main/assets/Flyers/von2_front.png new file mode 100644 index 0000000..ed5e37f Binary files /dev/null and b/android/app/src/main/assets/Flyers/von2_front.png differ diff --git a/android/app/src/main/assets/Flyers/vs298_back.png b/android/app/src/main/assets/Flyers/vs298_back.png new file mode 100644 index 0000000..41b346d Binary files /dev/null and b/android/app/src/main/assets/Flyers/vs298_back.png differ diff --git a/android/app/src/main/assets/Flyers/vs298_front.png b/android/app/src/main/assets/Flyers/vs298_front.png new file mode 100644 index 0000000..d68f161 Binary files /dev/null and b/android/app/src/main/assets/Flyers/vs298_front.png differ diff --git a/android/app/src/main/assets/Flyers/vs299_back.png b/android/app/src/main/assets/Flyers/vs299_back.png new file mode 100644 index 0000000..e6db22b Binary files /dev/null and b/android/app/src/main/assets/Flyers/vs299_back.png differ diff --git a/android/app/src/main/assets/Flyers/vs299_front.png b/android/app/src/main/assets/Flyers/vs299_front.png new file mode 100644 index 0000000..6af3ad4 Binary files /dev/null and b/android/app/src/main/assets/Flyers/vs299_front.png differ diff --git a/android/app/src/main/assets/Flyers/vs2_back.png b/android/app/src/main/assets/Flyers/vs2_back.png new file mode 100644 index 0000000..f278489 Binary files /dev/null and b/android/app/src/main/assets/Flyers/vs2_back.png differ diff --git a/android/app/src/main/assets/Flyers/vs2_front.png b/android/app/src/main/assets/Flyers/vs2_front.png new file mode 100644 index 0000000..713498d Binary files /dev/null and b/android/app/src/main/assets/Flyers/vs2_front.png differ diff --git a/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt b/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt index 512c7a0..24ebe7d 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/AssetInstaller.kt @@ -9,7 +9,7 @@ object AssetInstaller { // Copy the standard Supermodel folder layout from APK assets into the internal user root. // We never overwrite existing files so user edits persist. - val topLevel = listOf("Assets", "Config", "GraphicsAnalysis", "NVRAM", "Saves") + val topLevel = listOf("Assets", "Config", "GraphicsAnalysis", "NVRAM", "Saves", "Flyers") for (dir in topLevel) { copyAssetTree(context, dir, File(internalUserRoot, dir)) } diff --git a/android/app/src/main/java/com/izzy2lost/super3/FlyerBitmapLoader.kt b/android/app/src/main/java/com/izzy2lost/super3/FlyerBitmapLoader.kt new file mode 100644 index 0000000..5ae6181 --- /dev/null +++ b/android/app/src/main/java/com/izzy2lost/super3/FlyerBitmapLoader.kt @@ -0,0 +1,84 @@ +package com.izzy2lost.super3 + +import android.content.res.ColorStateList +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.LruCache +import android.widget.ImageView +import androidx.core.content.ContextCompat +import com.google.android.material.color.MaterialColors +import java.io.File +import java.util.concurrent.Executors + +object FlyerBitmapLoader { + private val executor = Executors.newFixedThreadPool(2) { r -> + Thread(r, "Super3FlyerDecode").apply { isDaemon = true } + } + + private val cache = + object : LruCache(16 * 1024) { + override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount / 1024 + } + + fun load(imageView: ImageView, file: File?, targetMaxPx: Int = 1200) { + if (file == null || !file.exists()) { + setPlaceholder(imageView) + imageView.tag = null + return + } + + val key = "${file.absolutePath}:${file.lastModified()}:${targetMaxPx}" + imageView.tag = key + cache.get(key)?.let { cached -> + clearTint(imageView) + imageView.setImageBitmap(cached) + return + } + + setPlaceholder(imageView) + executor.execute { + val bmp = decodeScaled(file, targetMaxPx) ?: return@execute + cache.put(key, bmp) + imageView.post { + if (imageView.tag == key) { + clearTint(imageView) + imageView.setImageBitmap(bmp) + } + } + } + } + + private fun decodeScaled(file: File, targetMaxPx: Int): Bitmap? { + val bounds = + BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeFile(file.absolutePath, bounds) + val srcW = bounds.outWidth + val srcH = bounds.outHeight + if (srcW <= 0 || srcH <= 0) return null + + val srcMax = maxOf(srcW, srcH) + var sample = 1 + while (srcMax / sample > targetMaxPx) sample *= 2 + + val opts = + BitmapFactory.Options().apply { + inSampleSize = sample + inPreferredConfig = Bitmap.Config.ARGB_8888 + } + + return BitmapFactory.decodeFile(file.absolutePath, opts) + } + + private fun setPlaceholder(imageView: ImageView) { + val color = MaterialColors.getColor(imageView, com.google.android.material.R.attr.colorOnSurfaceVariant) + imageView.imageTintList = ColorStateList.valueOf(color) + imageView.setImageDrawable(ContextCompat.getDrawable(imageView.context, R.drawable.image_24px)) + } + + private fun clearTint(imageView: ImageView) { + imageView.imageTintList = null + } +} + 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 648b573..713e80e 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageButton import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts @@ -14,10 +15,12 @@ import androidx.core.view.GravityCompat import androidx.core.widget.addTextChangedListener import androidx.documentfile.provider.DocumentFile import androidx.drawerlayout.widget.DrawerLayout +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.button.MaterialButton import com.google.android.material.color.MaterialColors +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationView import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchView @@ -26,6 +29,8 @@ import kotlin.math.max import kotlin.math.min import kotlin.concurrent.thread +enum class GameListViewMode { LIST, FLYERS } + class MainActivity : AppCompatActivity() { private val prefs by lazy { getSharedPreferences("super3_prefs", MODE_PRIVATE) } @@ -34,6 +39,7 @@ class MainActivity : AppCompatActivity() { private lateinit var toolbar: MaterialToolbar private lateinit var searchBar: SearchBar private lateinit var searchView: SearchView + private lateinit var btnViewMode: ImageButton private lateinit var gamesFolderText: TextView private lateinit var userFolderText: TextView @@ -54,6 +60,7 @@ class MainActivity : AppCompatActivity() { private var games: List = emptyList() private var zipDocs: Map = emptyMap() + private var viewMode: GameListViewMode = GameListViewMode.FLYERS @Volatile private var scanning = false @@ -111,6 +118,7 @@ class MainActivity : AppCompatActivity() { toolbar = findViewById(R.id.toolbar) searchBar = findViewById(R.id.search_bar) searchView = findViewById(R.id.search_view) + btnViewMode = findViewById(R.id.btn_view_mode) gamesList = findViewById(R.id.games_list) searchResultsList = findViewById(R.id.search_results_list) @@ -197,6 +205,17 @@ class MainActivity : AppCompatActivity() { gamesAdapter.setFilter(editable?.toString().orEmpty()) } + viewMode = loadViewMode() + btnViewMode.setOnClickListener { + viewMode = + when (viewMode) { + GameListViewMode.LIST -> GameListViewMode.FLYERS + GameListViewMode.FLYERS -> GameListViewMode.LIST + } + saveViewMode(viewMode) + applyViewMode(viewMode) + } + loadPrefs() games = GameXml.parseGamesXmlFromAssets(this) @@ -204,6 +223,7 @@ class MainActivity : AppCompatActivity() { applyVideoSettingsToIni(internalUserRoot(), loadVideoSettings()) + applyViewMode(viewMode) refreshUi() } @@ -221,6 +241,66 @@ class MainActivity : AppCompatActivity() { return File(getExternalFilesDir(null), "super3") } + private fun flyersDir(internalRoot: File = internalUserRoot()): File { + return File(internalRoot, "Flyers") + } + + private fun flyerSpanCount(): Int { + val dm = resources.displayMetrics + val portrait = dm.heightPixels >= dm.widthPixels + if (portrait) return 1 + val widthDp = dm.widthPixels / dm.density + val desiredItemDp = 190f + return (widthDp / desiredItemDp).toInt().coerceIn(2, 6) + } + + private fun applyViewMode(mode: GameListViewMode) { + gamesAdapter.setViewMode(mode) + + val lm = + when (mode) { + GameListViewMode.LIST -> androidx.recyclerview.widget.LinearLayoutManager(this) + GameListViewMode.FLYERS -> GridLayoutManager(this, flyerSpanCount()) + } + + gamesList.layoutManager = lm + searchResultsList.layoutManager = + when (mode) { + GameListViewMode.LIST -> androidx.recyclerview.widget.LinearLayoutManager(this) + GameListViewMode.FLYERS -> GridLayoutManager(this, flyerSpanCount()) + } + + btnViewMode.setImageResource( + when (mode) { + GameListViewMode.LIST -> R.drawable.view_list_24px + GameListViewMode.FLYERS -> R.drawable.view_module_24px + }, + ) + btnViewMode.contentDescription = + when (mode) { + GameListViewMode.LIST -> "List view" + GameListViewMode.FLYERS -> "Flyer view" + } + + gamesAdapter.submitList(buildItems(games, zipDocs, flyersDir())) + } + + private fun loadViewMode(): GameListViewMode { + return when (prefs.getString("ui_view_mode", null)) { + "list" -> GameListViewMode.LIST + else -> GameListViewMode.FLYERS + } + } + + private fun saveViewMode(mode: GameListViewMode) { + val v = + when (mode) { + GameListViewMode.LIST -> "list" + GameListViewMode.FLYERS -> "flyers" + } + prefs.edit().putString("ui_view_mode", v).apply() + } + private fun supermodelIniFile(internalRoot: File = internalUserRoot()): File { return File(File(internalRoot, "Config"), "Supermodel.ini") } @@ -286,7 +366,7 @@ class MainActivity : AppCompatActivity() { val tree = userTreeUri if (tree != null) { thread(name = "Super3SyncSettings") { - UserDataSync.syncInternalIntoTree(this, internalUserRoot(), tree) + UserDataSync.syncInternalIntoTree(this, internalUserRoot(), tree, UserDataSync.DIRS_SETTINGS_ONLY) } } } @@ -491,7 +571,7 @@ class MainActivity : AppCompatActivity() { val gamesUri = gamesTreeUri if (gamesUri == null) { zipDocs = emptyMap() - gamesAdapter.submitList(buildItems(games, zipDocs)) + gamesAdapter.submitList(buildItems(games, zipDocs, flyersDir())) statusText.text = "Games folder not set. Tap \"Games folder\" to choose." scanning = false return @@ -502,8 +582,12 @@ class MainActivity : AppCompatActivity() { statusText.text = "Scanning…" thread(name = "Super3Scanner") { + val userUri = userTreeUri + if (userUri != null) { + UserDataSync.syncFromTreeIntoInternal(this, userUri, internalUserRoot(), listOf("Flyers")) + } val zips = scanZipDocs(gamesUri) - val items = buildItems(games, zips) + val items = buildItems(games, zips, flyersDir()) runOnUiThread { zipDocs = zips gamesAdapter.submitList(items) @@ -540,7 +624,7 @@ class MainActivity : AppCompatActivity() { val userUri = userTreeUri if (userUri != null) { - UserDataSync.syncFromTreeIntoInternal(this, userUri, internalRoot) + UserDataSync.syncFromTreeIntoInternal(this, userUri, internalRoot, UserDataSync.DIRS_GAME_SYNC) } applyVideoSettingsToIni(internalRoot, loadVideoSettings()) @@ -621,11 +705,67 @@ class MainActivity : AppCompatActivity() { } } -private data class GameItem(val game: GameDef, val launchable: Boolean, val status: String) +private data class GameItem( + val game: GameDef, + val launchable: Boolean, + val status: String, + val flyerFront: File?, + val flyerBack: File?, + val hasOwnFlyer: Boolean, +) { + val hasFlyer: Boolean + get() = flyerFront != null +} -private fun buildItems(games: List, zipDocs: Map): List { +private data class FlyerGroup( + val rootName: String, + val groupName: String, + val title: String, + val flyerFront: File?, + val flyerBack: File?, + val variants: List, +) + +private fun flyerGroupRootAlias(name: String): String? { + return when { + name.equals("vs99.1", ignoreCase = true) -> "vs99" + name.equals("vs2v991", ignoreCase = true) -> "vs299" + else -> null + } +} + +private fun shouldForceGroupWithRoot(name: String): Boolean { + return name.equals("vs99.1", ignoreCase = true) || name.equals("vs2v991", ignoreCase = true) +} + +private fun flyerKeyCandidates(name: String): List { + val alias = flyerGroupRootAlias(name) ?: return listOf(name) + return if (alias == name) listOf(name) else listOf(alias, name) +} + +private fun buildItems(games: List, zipDocs: Map, flyersDir: File): List { val byName = games.associateBy { it.name } + val frontByKey = HashMap(128) + val backByKey = HashMap(128) + if (flyersDir.exists()) { + val files = flyersDir.listFiles().orEmpty() + for (f in files) { + if (!f.isFile) continue + val base = f.nameWithoutExtension + when { + base.endsWith("_front", ignoreCase = true) -> { + val key = base.dropLast("_front".length) + if (key.isNotBlank()) frontByKey[key] = f + } + base.endsWith("_back", ignoreCase = true) -> { + val key = base.dropLast("_back".length) + if (key.isNotBlank()) backByKey[key] = f + } + } + } + } + fun requiredZips(g: GameDef): List { val required = ArrayList(4) val visited = HashSet(8) @@ -640,6 +780,32 @@ private fun buildItems(games: List, zipDocs: Map) return required } + fun normalizeFlyers(front: File?, back: File?): Pair { + if (front == null && back == null) return null to null + val displayFront = front ?: back + val displayBack = if (front != null && back != null) back else null + return displayFront to displayBack + } + + fun resolveFlyers(g: GameDef): Pair { + val visited = HashSet(8) + var cur: GameDef? = g + while (cur != null) { + if (!visited.add(cur.name)) break + for (key in flyerKeyCandidates(cur.name)) { + val front = frontByKey[key] + val back = backByKey[key] + if (front != null || back != null) { + return normalizeFlyers(front, back) + } + } + val parent = cur.parent + if (parent.isNullOrBlank()) break + cur = byName[parent] + } + return null to null + } + val items = games.map { g -> val req = requiredZips(g) val missing = req.filter { !zipDocs.containsKey(it) } @@ -650,19 +816,47 @@ private fun buildItems(games: List, zipDocs: Map) } else { "missing ${missing.joinToString(" + ") { "${it}.zip" }}" } - GameItem(game = g, launchable = launchable, status = status) + val directFront = frontByKey[g.name] + val directBack = backByKey[g.name] + val hasOwnFlyer = directFront != null || directBack != null + val flyers = resolveFlyers(g) + GameItem( + game = g, + launchable = launchable, + status = status, + flyerFront = flyers.first, + flyerBack = flyers.second, + hasOwnFlyer = hasOwnFlyer, + ) } - return items.sortedWith(compareByDescending { it.launchable }.thenBy { it.game.displayName }) + return items.sortedWith( + compareByDescending { it.hasFlyer } + .thenByDescending { it.launchable } + .thenBy { it.game.displayName }, + ) } private class GamesAdapter( private val onClick: (GameItem) -> Unit, -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter() { + companion object { + private const val VT_LIST = 1 + private const val VT_FLYERS = 2 + } + + private var viewMode: GameListViewMode = GameListViewMode.FLYERS private var allItems: List = emptyList() private var shownItems: List = emptyList() + private var shownFlyerGroups: List = emptyList() private var filter: String = "" + fun setViewMode(mode: GameListViewMode) { + if (mode == viewMode) return + viewMode = mode + applyFilter() + } + fun submitList(items: List) { allItems = items applyFilter() @@ -677,31 +871,156 @@ private class GamesAdapter( private fun applyFilter() { val q = filter.trim() - shownItems = - if (q.isBlank()) { - allItems - } else { - val needle = q.lowercase() - allItems.filter { - it.game.displayName.lowercase().contains(needle) || it.game.name.lowercase().contains(needle) + if (viewMode == GameListViewMode.FLYERS) { + val needle = q.takeIf { it.isNotBlank() }?.lowercase() + shownFlyerGroups = buildFlyerGroups(allItems, needle) + } else { + shownItems = + if (q.isBlank()) { + allItems + } else { + val needle = q.lowercase() + allItems.filter { + it.game.displayName.lowercase().contains(needle) || it.game.name.lowercase().contains(needle) + } } - } + } notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_game, parent, false) - return VH(view) + override fun getItemViewType(position: Int): Int { + return when (viewMode) { + GameListViewMode.LIST -> VT_LIST + GameListViewMode.FLYERS -> VT_FLYERS + } } - override fun onBindViewHolder(holder: VH, position: Int) { - val item = shownItems[position] - holder.bind(item, onClick) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + VT_LIST -> ListVH(inflater.inflate(R.layout.item_game_list, parent, false)) + else -> FlyerGroupVH(inflater.inflate(R.layout.item_game, parent, false)) + } } - override fun getItemCount(): Int = shownItems.size + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is ListVH -> holder.bind(shownItems[position], onClick) + is FlyerGroupVH -> holder.bind(shownFlyerGroups[position], onClick) + } + } - class VH(itemView: View) : RecyclerView.ViewHolder(itemView) { + override fun getItemCount(): Int { + return if (viewMode == GameListViewMode.FLYERS) { + shownFlyerGroups.size + } else { + shownItems.size + } + } + + private fun buildFlyerGroups(items: List, needle: String?): List { + if (items.isEmpty()) return emptyList() + + val defByName = items.associate { it.game.name to it.game } + val rootCache = HashMap(items.size) + + fun rootNameFor(game: GameDef): String { + rootCache[game.name]?.let { return it } + val visited = HashSet(8) + var cur: GameDef? = game + while (cur != null) { + if (!visited.add(cur.name)) break + val parent = cur.parent + if (parent.isNullOrBlank()) break + cur = defByName[parent] + } + val root = cur?.name ?: game.name + val normalized = flyerGroupRootAlias(root) ?: root + rootCache[game.name] = normalized + return normalized + } + + fun matches(item: GameItem, needleLc: String): Boolean { + if (item.game.displayName.lowercase().contains(needleLc)) return true + if (item.game.name.lowercase().contains(needleLc)) return true + val ver = item.game.version + if (!ver.isNullOrBlank() && ver.lowercase().contains(needleLc)) return true + return false + } + + val groups = + items.groupBy { item -> + val root = rootNameFor(item.game) + val splitByOwnFlyer = + item.hasOwnFlyer && + item.game.name != root && + !shouldForceGroupWithRoot(item.game.name) + if (splitByOwnFlyer) item.game.name else root + } + val result = ArrayList(groups.size) + for ((groupName, groupItems) in groups) { + val groupGame = defByName[groupName] ?: groupItems.firstOrNull()?.game + val rootName = + if (groupGame != null) { + rootNameFor(groupGame) + } else { + groupName + } + + val title = + if (groupGame != null && groupName != rootName) { + groupGame.displayName + } else { + groupGame?.title ?: groupName + } + + val ordered = + groupItems.sortedWith( + compareByDescending { it.game.name == groupName } + .thenBy { it.game.displayName }, + ) + + val filteredVariants = + if (needle == null) { + ordered + } else { + val rootMatches = + title.lowercase().contains(needle) || + rootName.lowercase().contains(needle) + val matched = ordered.filter { matches(it, needle) } + when { + rootMatches -> ordered + matched.isNotEmpty() -> matched + else -> emptyList() + } + } + + if (filteredVariants.isEmpty()) continue + + val main = + ordered.firstOrNull { it.game.name == groupName } ?: ordered.firstOrNull() + val flyerFront = main?.flyerFront + val flyerBack = main?.flyerBack + + result.add( + FlyerGroup( + rootName = rootName, + groupName = groupName, + title = title, + flyerFront = flyerFront, + flyerBack = flyerBack, + variants = filteredVariants, + ), + ) + } + + return result.sortedWith( + compareByDescending { it.flyerFront != null } + .thenBy { it.title }, + ) + } + + class ListVH(itemView: View) : RecyclerView.ViewHolder(itemView) { private val title: TextView = itemView.findViewById(R.id.game_title) private val subtitle: TextView = itemView.findViewById(R.id.game_subtitle) @@ -715,4 +1034,102 @@ private class GamesAdapter( itemView.setOnClickListener { onClick(item) } } } + + class FlyerGroupVH(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val flyerContainer: View = itemView.findViewById(R.id.flyer_container) + private val flyerFront: android.widget.ImageView = itemView.findViewById(R.id.flyer_front) + private val flyerBack: android.widget.ImageView = itemView.findViewById(R.id.flyer_back) + private val title: TextView = itemView.findViewById(R.id.game_title) + private val variantsContainer: ViewGroup = itemView.findViewById(R.id.variants_container) + + private var showingBack = false + + fun bind(group: FlyerGroup, onClick: (GameItem) -> Unit) { + title.text = group.title + + val density = itemView.resources.displayMetrics.density + flyerFront.cameraDistance = 8000f * density + flyerBack.cameraDistance = 8000f * density + + showingBack = false + flyerFront.rotationY = 0f + flyerBack.rotationY = 0f + flyerFront.visibility = View.VISIBLE + flyerBack.visibility = View.GONE + + FlyerBitmapLoader.load(flyerFront, group.flyerFront, targetMaxPx = 1600) + FlyerBitmapLoader.load(flyerBack, group.flyerBack, targetMaxPx = 1600) + + val canFlip = group.flyerFront != null && group.flyerBack != null + flyerContainer.isClickable = canFlip + flyerContainer.isFocusable = canFlip + flyerContainer.setOnClickListener { + if (!canFlip) return@setOnClickListener + flip() + } + + variantsContainer.removeAllViews() + val inflater = LayoutInflater.from(itemView.context) + + for (variant in group.variants) { + val row = inflater.inflate(R.layout.item_flyer_variant, variantsContainer, false) + val tvTitle: TextView = row.findViewById(R.id.variant_title) + val tvSubtitle: TextView = row.findViewById(R.id.variant_subtitle) + val btnPlay: ImageButton = row.findViewById(R.id.btn_play_variant) + + val isPrimary = variant.game.name == group.groupName + val version = variant.game.version?.trim().orEmpty() + tvTitle.text = + when { + isPrimary && group.variants.size == 1 -> "Default" + version.isNotBlank() -> version + isPrimary -> "Default" + else -> variant.game.name + } + tvSubtitle.text = variant.status + + row.isEnabled = variant.launchable + row.alpha = if (variant.launchable) 1.0f else 0.5f + btnPlay.isEnabled = variant.launchable + + val click = View.OnClickListener { onClick(variant) } + row.setOnClickListener(click) + btnPlay.setOnClickListener(click) + + variantsContainer.addView(row) + } + } + + private fun flip() { + val front = flyerFront + val back = flyerBack + + val first = if (showingBack) back else front + val second = if (showingBack) front else back + val toBack = !showingBack + showingBack = toBack + + val duration = 140L + val flipOut = android.animation.ObjectAnimator.ofFloat(first, View.ROTATION_Y, 0f, 90f).apply { + this.duration = duration + } + val flipIn = android.animation.ObjectAnimator.ofFloat(second, View.ROTATION_Y, -90f, 0f).apply { + this.duration = duration + } + + flipOut.addListener( + object : android.animation.AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: android.animation.Animator) { + first.visibility = View.GONE + first.rotationY = 0f + second.visibility = View.VISIBLE + second.rotationY = -90f + flipIn.start() + } + }, + ) + + flipOut.start() + } + } } diff --git a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt index 5c6ca0e..d8e11b7 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt @@ -351,7 +351,7 @@ class Super3Activity : SDLActivity() { val treeUri = Uri.parse(uriStr) val internalRoot = File(getExternalFilesDir(null), "super3") thread(name = "Super3UserSync") { - UserDataSync.syncInternalIntoTree(this, internalRoot, treeUri) + UserDataSync.syncInternalIntoTree(this, internalRoot, treeUri, UserDataSync.DIRS_GAME_SYNC) } } @@ -523,7 +523,7 @@ class Super3Activity : SDLActivity() { outFile.parentFile?.mkdirs() val scaled = scaleDown(bitmap, targetWidth = 640) FileOutputStream(outFile).use { fos -> - scaled.compress(Bitmap.CompressFormat.PNG, 90, fos) + scaled.compress(Bitmap.CompressFormat.PNG, 100, fos) } if (scaled !== bitmap) scaled.recycle() } diff --git a/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt b/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt index 07fb744..6221cb3 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/UserDataSync.kt @@ -7,20 +7,31 @@ import androidx.documentfile.provider.DocumentFile import java.io.File object UserDataSync { - private val syncDirs = listOf("NVRAM", "Saves", "Config") + val DIRS_GAME_SYNC = listOf("NVRAM", "Saves", "Config", "Flyers") + val DIRS_SETTINGS_ONLY = listOf("Config") - fun syncFromTreeIntoInternal(context: Context, treeUri: Uri, internalUserRoot: File) { + fun syncFromTreeIntoInternal( + context: Context, + treeUri: Uri, + internalUserRoot: File, + dirs: List = DIRS_GAME_SYNC, + ) { val tree = DocumentFile.fromTreeUri(context, treeUri) ?: return - for (dirName in syncDirs) { + for (dirName in dirs) { val from = tree.findFile(dirName) ?: continue val to = File(internalUserRoot, dirName) copyDocDirToFileDir(context.contentResolver, from, to) } } - fun syncInternalIntoTree(context: Context, internalUserRoot: File, treeUri: Uri) { + fun syncInternalIntoTree( + context: Context, + internalUserRoot: File, + treeUri: Uri, + dirs: List = DIRS_GAME_SYNC, + ) { val tree = DocumentFile.fromTreeUri(context, treeUri) ?: return - for (dirName in syncDirs) { + for (dirName in dirs) { val from = File(internalUserRoot, dirName) if (!from.exists() || !from.isDirectory) continue val to = ensureDocDir(tree, dirName) @@ -79,4 +90,3 @@ object UserDataSync { } } } - diff --git a/android/app/src/main/res/drawable/branding_watermark_24px.xml b/android/app/src/main/res/drawable/branding_watermark_24px.xml new file mode 100644 index 0000000..7b2d9e9 --- /dev/null +++ b/android/app/src/main/res/drawable/branding_watermark_24px.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/drawable/drawer_background_rounded.xml b/android/app/src/main/res/drawable/drawer_background_rounded.xml new file mode 100644 index 0000000..cd5454c --- /dev/null +++ b/android/app/src/main/res/drawable/drawer_background_rounded.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/android/app/src/main/res/drawable/image_24px.xml b/android/app/src/main/res/drawable/image_24px.xml new file mode 100644 index 0000000..4eddb4f --- /dev/null +++ b/android/app/src/main/res/drawable/image_24px.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/drawable/play_circle_24px.xml b/android/app/src/main/res/drawable/play_circle_24px.xml new file mode 100644 index 0000000..bc3ff87 --- /dev/null +++ b/android/app/src/main/res/drawable/play_circle_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ripple_rounded_18.xml b/android/app/src/main/res/drawable/ripple_rounded_18.xml new file mode 100644 index 0000000..188e0c5 --- /dev/null +++ b/android/app/src/main/res/drawable/ripple_rounded_18.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable/view_list_24px.xml b/android/app/src/main/res/drawable/view_list_24px.xml new file mode 100644 index 0000000..70e34e0 --- /dev/null +++ b/android/app/src/main/res/drawable/view_list_24px.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/drawable/view_module_24px.xml b/android/app/src/main/res/drawable/view_module_24px.xml new file mode 100644 index 0000000..145d1cb --- /dev/null +++ b/android/app/src/main/res/drawable/view_module_24px.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 89d73b6..727b7ed 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -27,15 +27,34 @@ app:navigationIcon="@drawable/ic_menu_24" app:navigationIconTint="?attr/colorOnPrimary"> - + android:layout_marginEnd="8dp"> + + + + + + @@ -85,7 +104,7 @@ android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" - android:background="?attr/colorPrimary" + android:background="@drawable/drawer_background_rounded" app:menu="@menu/drawer_menu" app:headerLayout="@layout/nav_header" app:itemTextColor="?attr/colorOnPrimary" diff --git a/android/app/src/main/res/layout/dialog_screenshot_preview.xml b/android/app/src/main/res/layout/dialog_screenshot_preview.xml index c8593f3..c06707b 100644 --- a/android/app/src/main/res/layout/dialog_screenshot_preview.xml +++ b/android/app/src/main/res/layout/dialog_screenshot_preview.xml @@ -1,5 +1,6 @@ - + app:cardCornerRadius="18dp" + app:cardBackgroundColor="@color/screenshot_backdrop" + app:cardElevation="0dp" + app:strokeWidth="0dp" + app:contentPadding="4dp"> + + + + diff --git a/android/app/src/main/res/layout/item_flyer_variant.xml b/android/app/src/main/res/layout/item_flyer_variant.xml new file mode 100644 index 0000000..bcd436e --- /dev/null +++ b/android/app/src/main/res/layout/item_flyer_variant.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_game.xml b/android/app/src/main/res/layout/item_game.xml index 7c2026a..890690a 100644 --- a/android/app/src/main/res/layout/item_game.xml +++ b/android/app/src/main/res/layout/item_game.xml @@ -9,31 +9,66 @@ app:cardCornerRadius="12dp" app:cardBackgroundColor="?attr/colorSurface"> - + android:padding="12dp"> + + + + + + + + - - + android:textAppearance="?attr/textAppearanceTitleMedium" + android:textColor="?attr/colorOnSurface" + app:layout_constraintTop_toBottomOf="@id/flyer_container" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> - + + + diff --git a/android/app/src/main/res/layout/item_game_list.xml b/android/app/src/main/res/layout/item_game_list.xml new file mode 100644 index 0000000..d9220ba --- /dev/null +++ b/android/app/src/main/res/layout/item_game_list.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_save_slot.xml b/android/app/src/main/res/layout/item_save_slot.xml index 281165b..060f300 100644 --- a/android/app/src/main/res/layout/item_save_slot.xml +++ b/android/app/src/main/res/layout/item_save_slot.xml @@ -8,7 +8,7 @@ android:gravity="center_vertical" android:foreground="?attr/selectableItemBackground"> - + android:padding="2dp" + app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay_Super3_Rounded12" /> #6F7880 #C0C8CF - + #39FF14 + + #22000000 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index fe17362..813b3bd 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,4 +5,31 @@ @dimen/m3_comp_button_elevated_container_elevation @style/ThemeOverlay.Material3Expressive.Button.Elevated + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index 55e1363..daae69f 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -27,5 +27,9 @@ @style/Widget.Material3Expressive.Button @style/Widget.Super3.Button.ElevatedButton + + @style/ShapeAppearance.Super3.SmallComponent + @style/ShapeAppearance.Super3.MediumComponent + @style/ShapeAppearance.Super3.LargeComponent