From d29669333c10af5d63ac19e7dc66593bcf51f023 Mon Sep 17 00:00:00 2001 From: cjee21 <77721854+cjee21@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:14:27 +0800 Subject: [PATCH 1/7] Android TV app --- Source/GUI/Android/build.gradle | 1 + Source/GUI/Android/settings.gradle | 1 + Source/GUI/Android/tvapp/.gitignore | 1 + Source/GUI/Android/tvapp/CMakeLists.txt | 25 ++ Source/GUI/Android/tvapp/build.gradle.kts | 104 +++++++ Source/GUI/Android/tvapp/proguard-rules.pro | 21 ++ .../tvapp/src/main/AndroidManifest.xml | 49 ++++ .../java/net/mediaarea/mediainfo/MediaInfo.kt | 70 +++++ .../java/net/mediaarea/mediainfo/tv/Core.kt | 54 ++++ .../mediaarea/mediainfo/tv/MainActivity.kt | 178 ++++++++++++ .../mediaarea/mediainfo/tv/MainViewModel.kt | 28 ++ .../mediainfo/tv/MediaInfoApplication.kt | 15 + .../mediainfo/tv/ViewModelFactory.kt | 47 ++++ .../mediainfo/tv/feature/about/About.kt | 93 ++++++ .../mediainfo/tv/feature/browse/Browse.kt | 266 ++++++++++++++++++ .../tv/feature/browse/MediaRepository.kt | 97 +++++++ .../tv/feature/browse/MediaViewModel.kt | 57 ++++ .../tv/feature/report/ReportActivity.kt | 204 ++++++++++++++ .../tv/feature/report/ReportViewModel.kt | 79 ++++++ .../mediainfo/tv/feature/settings/Settings.kt | 149 ++++++++++ .../tv/feature/settings/SettingsRepository.kt | 92 ++++++ .../tv/feature/settings/SettingsViewModel.kt | 70 +++++ .../tv/ui/components/CategoryHeader.kt | 79 ++++++ .../tv/ui/components/MediaListItem.kt | 105 +++++++ .../tv/ui/components/MultiOptionListItem.kt | 154 ++++++++++ .../tv/ui/components/ToggleListItem.kt | 108 +++++++ .../mediaarea/mediainfo/tv/ui/theme/Color.kt | 17 ++ .../mediaarea/mediainfo/tv/ui/theme/Theme.kt | 47 ++++ .../mediaarea/mediainfo/tv/ui/theme/Type.kt | 42 +++ .../src/main/res/drawable/ic_action_add.xml | 10 + .../res/drawable/ic_banner_foreground.xml | 116 ++++++++ .../res/drawable/ic_launcher_foreground.xml | 82 ++++++ .../res/drawable/ic_launcher_monochrome.xml | 68 +++++ .../src/main/res/drawable/ic_menu_export.xml | 10 + .../src/main/res/drawable/ic_menu_view.xml | 10 + .../src/main/res/drawable/ic_report_close.xml | 10 + .../main/res/mipmap-anydpi-v26/ic_banner.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../res/mipmap-anydpi-v33/ic_launcher.xml | 6 + .../mipmap-anydpi-v33/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1084 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 2016 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2328 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 758 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 1306 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1508 bytes .../src/main/res/mipmap-xhdpi/ic_banner.png | Bin 0 -> 4964 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1394 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 3120 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3320 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2110 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 5014 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5120 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 2966 bytes .../ic_launcher_foreground.webp | Bin 0 -> 7260 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7540 bytes .../main/res/values/ic_banner_background.xml | 4 + .../res/values/ic_launcher_background.xml | 4 + .../tvapp/src/main/res/values/strings.xml | 37 +++ .../tvapp/src/main/res/values/themes.xml | 4 + 61 files changed, 2635 insertions(+) create mode 100644 Source/GUI/Android/tvapp/.gitignore create mode 100644 Source/GUI/Android/tvapp/CMakeLists.txt create mode 100644 Source/GUI/Android/tvapp/build.gradle.kts create mode 100644 Source/GUI/Android/tvapp/proguard-rules.pro create mode 100644 Source/GUI/Android/tvapp/src/main/AndroidManifest.xml create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/MediaInfo.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/Core.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainActivity.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainViewModel.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MediaInfoApplication.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ViewModelFactory.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/about/About.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/Browse.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaRepository.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaViewModel.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportActivity.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportViewModel.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/Settings.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsRepository.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsViewModel.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/CategoryHeader.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MediaListItem.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MultiOptionListItem.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/ToggleListItem.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Color.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Theme.kt create mode 100644 Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Type.kt create mode 100644 Source/GUI/Android/tvapp/src/main/res/drawable/ic_action_add.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/drawable/ic_banner_foreground.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_monochrome.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_export.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_view.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/drawable/ic_report_close.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_banner.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xhdpi/ic_banner.png create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 Source/GUI/Android/tvapp/src/main/res/values/ic_banner_background.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/values/ic_launcher_background.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/values/strings.xml create mode 100644 Source/GUI/Android/tvapp/src/main/res/values/themes.xml diff --git a/Source/GUI/Android/build.gradle b/Source/GUI/Android/build.gradle index b6034bb408..6bc106f281 100644 --- a/Source/GUI/Android/build.gradle +++ b/Source/GUI/Android/build.gradle @@ -19,6 +19,7 @@ buildscript { plugins { id 'com.google.devtools.ksp' version "$kotlin_version-2.0.4" apply false + id 'org.jetbrains.kotlin.plugin.compose' version "$kotlin_version" apply false } allprojects { diff --git a/Source/GUI/Android/settings.gradle b/Source/GUI/Android/settings.gradle index e7b4def49c..faea8eb94d 100644 --- a/Source/GUI/Android/settings.gradle +++ b/Source/GUI/Android/settings.gradle @@ -1 +1,2 @@ include ':app' +include ':tvapp' diff --git a/Source/GUI/Android/tvapp/.gitignore b/Source/GUI/Android/tvapp/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/Source/GUI/Android/tvapp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/CMakeLists.txt b/Source/GUI/Android/tvapp/CMakeLists.txt new file mode 100644 index 0000000000..f529b1dae3 --- /dev/null +++ b/Source/GUI/Android/tvapp/CMakeLists.txt @@ -0,0 +1,25 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +cmake_minimum_required(VERSION 3.5.0...4.3.0) + +project(MediaInfo) + +# Build MediaInfoLib +set(BUILD_SHARED_LIBS ON CACHE BOOL "Force shared library build" FORCE) +set(LARGE_FILES OFF CACHE BOOL "Enable large files support" FORCE) +set(BUILD_ZENLIB ON CACHE BOOL "Build bundled ZenLib" FORCE) + +add_subdirectory(../../../../../MediaInfoLib/Project/CMake ${CMAKE_CURRENT_BINARY_DIR}/MediaInfoLib) + +# disable warning in aes_via_ace.h for x86 arch +if(NOT "${CMAKE_VERSION}" VERSION_LESS "3.18") + set_property( + SOURCE + "../../../../../MediaInfoLib/Source/ThirdParty/aes-gladman/aes_modes.c" + "../../../../../MediaInfoLib/Source/ThirdParty/aes-gladman/aeskey.c" + DIRECTORY "../../../../../MediaInfoLib/Project/CMake" + APPEND + PROPERTY COMPILE_OPTIONS "-Wno-qualified-void-return-type" + ) +endif() diff --git a/Source/GUI/Android/tvapp/build.gradle.kts b/Source/GUI/Android/tvapp/build.gradle.kts new file mode 100644 index 0000000000..9662d51d43 --- /dev/null +++ b/Source/GUI/Android/tvapp/build.gradle.kts @@ -0,0 +1,104 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + namespace = "net.mediaarea.mediainfo.tv" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "net.mediaarea.mediainfo.tv" + minSdk = 23 + targetSdk = 36 + versionCode = 1 + versionName = "25.10" + @Suppress("UnstableApiUsage") + externalNativeBuild { + cmake { + arguments( + "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", + "-DENABLE_UNICODE=ON", + "-DMEDIAINFO_ADVANCED=OFF", + "-DMEDIAINFO_AES=OFF", + "-DMEDIAINFO_DEMUX=OFF", + "-DMEDIAINFO_DIRECTORY=OFF", + "-DMEDIAINFO_DUPLICATE=OFF", + "-DMEDIAINFO_DVDIF_ANALYZE=OFF", + "-DMEDIAINFO_EVENTS=OFF", + "-DMEDIAINFO_FILTER=OFF", + "-DMEDIAINFO_FIXITY=OFF", + "-DMEDIAINFO_GRAPH=OFF", + "-DMEDIAINFO_IBI=OFF", + "-DMEDIAINFO_LIBCURL=OFF", + "-DMEDIAINFO_LIBMMS=OFF", + "-DMEDIAINFO_MACROBLOCKS=OFF", + "-DMEDIAINFO_MD5=OFF", + "-DMEDIAINFO_NEXTPACKET=OFF", + "-DMEDIAINFO_READER=OFF", + "-DMEDIAINFO_READTHREAD=OFF", + "-DMEDIAINFO_REFERENCES=OFF", + "-DMEDIAINFO_SHA1=OFF", + "-DMEDIAINFO_SHA2=OFF", + "-DMEDIAINFO_TRACE=OFF", + "-DMEDIAINFO_TRACE_FFV1CONTENT=OFF" + ) + } + } + } + externalNativeBuild { + cmake { + path = file("CMakeLists.txt") + } + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } + } + buildFeatures { + buildConfig = true + compose = true + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.18.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation(platform("androidx.compose:compose-bom:2026.03.00")) + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.datastore:datastore-preferences:1.2.1") + implementation("androidx.tv:tv-foundation:1.0.0-beta01") + implementation("androidx.tv:tv-material:1.0.1") + implementation("androidx.lifecycle:lifecycle-process:2.10.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") + implementation("androidx.activity:activity-compose:1.13.0") + implementation("androidx.navigation:navigation-compose:2.9.7") + implementation("com.google.accompanist:accompanist-permissions:0.37.3") + androidTestImplementation(platform("androidx.compose:compose-bom:2026.03.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/Source/GUI/Android/tvapp/proguard-rules.pro b/Source/GUI/Android/tvapp/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/Source/GUI/Android/tvapp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/AndroidManifest.xml b/Source/GUI/Android/tvapp/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0e5e321aba --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/MediaInfo.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/MediaInfo.kt new file mode 100644 index 0000000000..839da2165d --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/MediaInfo.kt @@ -0,0 +1,70 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo + +class MediaInfo { + companion object { + // load the native library. + init { + System.loadLibrary("mediainfo") + } + } + + enum class Stream { + General, + Video, + Audio, + Text, + Other, + Image, + Menu, + Max + } + + enum class Info { + Name, + Text, + Measure, + Options, + Name_Text, + Measure_Text, + Info, + HowTo, + Domain, + Max + } + + val mi: Long = Init() + + external fun Init(): Long + external fun Destroy(): Int + private external fun OpenFd(fd: Int, name: String): Int + private external fun Open(name:String ): Int + fun Open(fd: Int, name: String): Int { + return OpenFd(fd, name) + } + external fun Open_Buffer_Init(fileSize: Long, fileOffset: Long): Int + external fun Open_Buffer_Continue(buffer: ByteArray, bufferSize: Long): Int + external fun Open_Buffer_Continue_GoTo_Get(): Long + external fun Open_Buffer_Finalize(): Long + external fun Close(): Int + external fun Inform(): String + private external fun GetI(streamKind: Int, streamNumber: Int, parameter: Int, infoKind: Int): String + private external fun GetS(streamKind: Int, streamNumber: Int, parameter: String, infoKind: Int, searchKind: Int): String + fun Get(streamKind: Stream, streamNumber: Int, parameter: Int, infoKind: Info = Info.Text) : String { + return GetI(streamKind.ordinal, streamNumber, parameter, infoKind.ordinal) + } + fun Get(streamKind: Stream, streamNumber: Int, parameter: String, infoKind: Info = Info.Text, searchKind: Info = Info.Name) : String { + return GetS(streamKind.ordinal, streamNumber, parameter, infoKind.ordinal, searchKind.ordinal) + } + external fun Option(option: String, value: String = ""): String + external fun State_Get(): Int + private external fun Count_Get(streamKind: Int, streamNumber: Int): Int + fun Count_Get(streamKind: Stream, streamNumber: Int = -1): Int { + return Count_Get(streamKind.ordinal, streamNumber) + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/Core.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/Core.kt new file mode 100644 index 0000000000..a5dc66ab12 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/Core.kt @@ -0,0 +1,54 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv + +import net.mediaarea.mediainfo.MediaInfo +import net.mediaarea.mediainfo.tv.feature.settings.DisplayCaptions + +object Core { + data class ReportView( + val name: String, + val desc: String, + val mime: String, + val exportable: Boolean + ) { + override fun toString(): String { + return desc + } + } + + val mi: MediaInfo = MediaInfo() + val views: MutableList = mutableListOf() + val version: String = mi.Option("Info_Version").replace("MediaInfoLib - v", "") + + init { + // populate views list + val viewsCsv: String = mi.Option("Info_OutputFormats_CSV") + viewsCsv.split("\n").forEach { + val view: List = it.split(",") + if (view.size > 2) + views.add(ReportView(view[0], view[1], view[2], true)) + } + } + + fun generateReport( + fd: Int, + name: String, + legacyStreamDisplay: Boolean, + displayCaptions: DisplayCaptions + ): String { + mi.Option("Inform", "HTML") + mi.Option("LegacyStreamDisplay", if (legacyStreamDisplay) "1" else "0") + mi.Option("File_DisplayCaptions", displayCaptions.key) + + mi.Open(fd, name) + val report = mi.Inform() + mi.Close() + + return report + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainActivity.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainActivity.kt new file mode 100644 index 0000000000..0a6b549e28 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainActivity.kt @@ -0,0 +1,178 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.tv.material3.DrawerState +import androidx.tv.material3.DrawerValue +import androidx.tv.material3.Icon +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.NavigationDrawer +import androidx.tv.material3.NavigationDrawerItem +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import androidx.tv.material3.rememberDrawerState +import androidx.tv.material3.surfaceColorAtElevation +import net.mediaarea.mediainfo.tv.feature.about.About +import net.mediaarea.mediainfo.tv.feature.browse.Browse +import net.mediaarea.mediainfo.tv.feature.settings.Settings +import net.mediaarea.mediainfo.tv.ui.theme.AndroidTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MediaInfoAppContainer() + } + } +} + +@Composable +private fun MediaInfoAppContainer() { + val app = LocalContext.current.applicationContext as MediaInfoApplication + val viewModel: MainViewModel = viewModel(factory = app.factory) + + // Collect the state from the ViewModel in a lifecycle-aware way + val isDark by viewModel.darkModeEnabled.collectAsStateWithLifecycle() + + AndroidTheme(isInDarkTheme = isDark) { + MediaInfoApp() + } +} + +@Composable +private fun MediaInfoApp(drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Open)) { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + val items = + listOf( + Screen.Browse, + Screen.Settings, + Screen.About + ) + + NavigationDrawer( + drawerState = drawerState, + drawerContent = { drawerValue -> + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(0.dp)) + .fillMaxHeight() + .padding(12.dp) + .selectableGroup(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically) + ) { + items.forEach { screen -> + NavigationDrawerItem( + selected = currentRoute == screen.route, + onClick = { + navController.navigate(screen.route) { + // Pop up to start destination to avoid building up a large stack + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + leadingContent = { + Icon(imageVector = screen.icon, contentDescription = null) + } + ) { + Text(stringResource(screen.resourceId)) + } + } + } + if (drawerValue == DrawerValue.Open) { + MediaInfoHeader(Modifier.align(Alignment.TopStart)) + } + } + ) { + NavHost(navController, startDestination = Screen.Browse.route) { + composable(Screen.Browse.route) { Browse() } + composable(Screen.Settings.route) { Settings() } + composable(Screen.About.route) { About() } + } + } + } +} + +private sealed class Screen( + val route: String, + @get:StringRes val resourceId: Int, + val icon: ImageVector +) { + object Browse : Screen("browse", R.string.browse, Icons.Default.Folder) + object Settings : Screen("settings", R.string.settings, Icons.Default.Settings) + object About : Screen("about", R.string.about, Icons.Default.Info) +} + +@Composable +private fun MediaInfoHeader(modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(20.dp) + ) { + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.headlineSmall, + ) + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun MediaInfoAppPreview() { + AndroidTheme { + MediaInfoApp() + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun MediaInfoAppPreviewDrawerClosed() { + AndroidTheme { + MediaInfoApp(rememberDrawerState(initialValue = DrawerValue.Closed)) + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainViewModel.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainViewModel.kt new file mode 100644 index 0000000000..0e89825634 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MainViewModel.kt @@ -0,0 +1,28 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// From code generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import net.mediaarea.mediainfo.tv.feature.settings.FLOW_TIMEOUT +import net.mediaarea.mediainfo.tv.feature.settings.SettingsRepository + +class MainViewModel(repository: SettingsRepository) : ViewModel() { + val darkModeEnabled: StateFlow = repository.settingsFlow + .map { it.darkModeEnabled } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(FLOW_TIMEOUT), + initialValue = true + ) +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MediaInfoApplication.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MediaInfoApplication.kt new file mode 100644 index 0000000000..a37d9b81af --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/MediaInfoApplication.kt @@ -0,0 +1,15 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv + +import android.app.Application +import net.mediaarea.mediainfo.tv.feature.settings.SettingsRepository + +class MediaInfoApplication : Application() { + val settingsRepository by lazy { SettingsRepository(applicationContext) } + val factory by lazy { ViewModelFactory(this, settingsRepository) } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ViewModelFactory.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ViewModelFactory.kt new file mode 100644 index 0000000000..82ecebe0d8 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ViewModelFactory.kt @@ -0,0 +1,47 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// From code generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import net.mediaarea.mediainfo.tv.feature.browse.MediaRepository +import net.mediaarea.mediainfo.tv.feature.browse.MediaViewModel +import net.mediaarea.mediainfo.tv.feature.report.ReportViewModel +import net.mediaarea.mediainfo.tv.feature.settings.SettingsRepository +import net.mediaarea.mediainfo.tv.feature.settings.SettingsViewModel + +class ViewModelFactory( + private val application: Application, + private val repository: SettingsRepository +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return when { + + modelClass.isAssignableFrom(MainViewModel::class.java) -> { + MainViewModel(repository) as T + } + + modelClass.isAssignableFrom(SettingsViewModel::class.java) -> { + SettingsViewModel(repository) as T + } + + modelClass.isAssignableFrom(MediaViewModel::class.java) -> { + MediaViewModel(MediaRepository(application.applicationContext), repository) as T + } + + modelClass.isAssignableFrom(ReportViewModel::class.java) -> { + ReportViewModel(application.contentResolver, repository) as T + } + + else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/about/About.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/about/About.kt new file mode 100644 index 0000000000..26f2064300 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/about/About.kt @@ -0,0 +1,93 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.feature.about + +import androidx.compose.foundation.Image +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import net.mediaarea.mediainfo.tv.BuildConfig +import net.mediaarea.mediainfo.tv.Core +import net.mediaarea.mediainfo.tv.R +import net.mediaarea.mediainfo.tv.ui.theme.AndroidTheme + +@Composable +fun About() { + AboutContent(mediainfoLibVersion = Core.version) +} + +@Composable +private fun AboutContent( + mediainfoLibVersion: String +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(80.dp) + .focusable(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + val modifier = Modifier.padding(10.dp) + Image( + painter = painterResource(id = R.mipmap.ic_launcher_foreground), + contentDescription = stringResource(R.string.app_name) + " icon", + ) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.headlineMedium, + modifier = modifier, + ) + Text( + text = stringResource(R.string.about_about_text) + .replace("%MI_VERSION%", BuildConfig.VERSION_NAME) + .replace("%MIL_VERSION%", mediainfoLibVersion), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = modifier, + ) + Text( + text = stringResource(R.string.about_description_text), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = modifier, + ) + Text( + text = stringResource(R.string.about_copyright_text), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = modifier, + ) + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun AboutPreview() { + AndroidTheme { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + AboutContent(BuildConfig.VERSION_NAME) + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/Browse.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/Browse.kt new file mode 100644 index 0000000000..877ec3f099 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/Browse.kt @@ -0,0 +1,266 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.feature.browse + +import android.Manifest +import android.content.Intent +import android.os.Build +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Movie +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.tv.material3.Button +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import net.mediaarea.mediainfo.tv.MediaInfoApplication +import net.mediaarea.mediainfo.tv.R +import net.mediaarea.mediainfo.tv.feature.report.ReportActivity +import net.mediaarea.mediainfo.tv.ui.components.MediaListItem +import net.mediaarea.mediainfo.tv.ui.theme.AndroidTheme + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun Browse() { + val permissionsState = rememberMultiplePermissionsState( + permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_AUDIO + ) + } else { + listOf(Manifest.permission.READ_EXTERNAL_STORAGE) + } + ) + + if (permissionsState.allPermissionsGranted) { + MediaBrowserScreen() + } else { + PermissionDeniedNotice { permissionsState.launchMultiplePermissionRequest() } + } +} + +@Composable +private fun MediaBrowserScreen() { + val app = LocalContext.current.applicationContext as MediaInfoApplication + val viewModel: MediaViewModel = viewModel(factory = app.factory) + val state by viewModel.uiState.collectAsState() + + // Fetch MediaStore at least once or on app process resume + LifecycleResumeEffect( + key1 = Unit, + lifecycleOwner = ProcessLifecycleOwner.get() + ) { + viewModel.loadMedia() + onPauseOrDispose {} + } + + MediaBrowserScreenContent(state) +} + +@Composable +private fun MediaBrowserScreenContent(state: MediaUiState) { + val context = LocalContext.current + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + when (state) { + is MediaUiState.Loading -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + CircularProgressIndicator() + Text(stringResource(R.string.loading)) + } + } + + is MediaUiState.Empty -> { + Text(stringResource(R.string.empty)) + } + + is MediaUiState.Error -> { + Text(stringResource(R.string.error) + ": " + state.message) + } + + is MediaUiState.Success -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(32.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(state.items) { item -> + MediaListItem( + name = item.name, + icon = when (item.type) { + MediaType.Video -> Icons.Default.Movie + MediaType.Audio -> Icons.Default.MusicNote + MediaType.Image -> Icons.Default.Image + }, + path = item.path, + date = item.dateModified, + onClick = { + val intent = Intent(context, ReportActivity::class.java).apply { + data = item.uri + putExtra("COMPLETE_NAME", item.path) + putExtra("FILE_NAME", item.name) + } + context.startActivity(intent) + } + ) + } + } + } + } + } +} + +@Composable +private fun PermissionDeniedNotice(onRequestPermission: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.permission_header), + style = MaterialTheme.typography.headlineMedium, + color = Color.White + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.permission_text, stringResource(R.string.app_name)), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 48.dp) + ) + Spacer(modifier = Modifier.height(32.dp)) + Button(onClick = onRequestPermission) { + Text(stringResource(R.string.permission_button)) + } + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun PermissionDeniedNoticePreview() { + AndroidTheme { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + PermissionDeniedNotice {} + } + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun MediaBrowserScreenLoadingPreview() { + AndroidTheme { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + MediaBrowserScreenContent(MediaUiState.Loading) + } + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun MediaBrowserScreenSuccessPreview() { + val testMediaList = listOf( + MediaItem( + id = 1L, + uri = "content://media/external/images/media/1".toUri(), + name = "Test image.jpg", + type = MediaType.Image, + path = "/storage/emulated/0/Pictures/Test image.jpg", + dateModified = 1767877545000L + ), + MediaItem( + id = 2L, + uri = "content://media/external/video/media/2".toUri(), + name = "Test video.mkv", + type = MediaType.Video, + path = "/storage/emulated/0/Movies/Test video.mkv", + dateModified = 1767865330000L + ), + MediaItem( + id = 3L, + uri = "content://media/external/audio/media/3".toUri(), + name = "Test audio.flac", + type = MediaType.Audio, + path = "/storage/emulated/0/Music/Recordings/Test audio.flac", + dateModified = 1767856522000L + ) + ) + AndroidTheme { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + MediaBrowserScreenContent(MediaUiState.Success(testMediaList)) + } + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun MediaBrowserScreenEmptyPreview() { + AndroidTheme { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + MediaBrowserScreenContent(MediaUiState.Empty) + } + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun MediaBrowserScreenErrorPreview() { + AndroidTheme { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + MediaBrowserScreenContent(MediaUiState.Error("Test error")) + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaRepository.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaRepository.kt new file mode 100644 index 0000000000..1dc3c14a7b --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaRepository.kt @@ -0,0 +1,97 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// From code generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv.feature.browse + +import android.annotation.SuppressLint +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.mediaarea.mediainfo.tv.feature.settings.SortOrder +import kotlin.time.Duration.Companion.seconds + +data class MediaItem( + val id: Long, + val uri: Uri, + val name: String, + val type: MediaType, + val path: String, + val dateModified: Long +) + +enum class MediaType { Image, Video, Audio } + +class MediaRepository(private val context: Context) { + + @SuppressLint("InlinedApi") + suspend fun fetchMedia( + sortOrderSetting: SortOrder, + dispatcher: CoroutineDispatcher = Dispatchers.IO + ): List = withContext(dispatcher) { + + val mediaList = mutableListOf() + + val collection = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) + + val projection = arrayOf( + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.DISPLAY_NAME, + MediaStore.Files.FileColumns.MEDIA_TYPE, + MediaStore.Files.FileColumns.DATA, + MediaStore.Files.FileColumns.DATE_MODIFIED + ) + + val selection = "${MediaStore.Files.FileColumns.MEDIA_TYPE} IN (?, ?, ?)" + val selectionArgs = arrayOf( + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString(), + MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO.toString() + ) + + val sortOrder = when (sortOrderSetting) { + SortOrder.DATE -> "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC" + SortOrder.PATH -> "${MediaStore.Files.FileColumns.DATA} ASC" + SortOrder.NAME -> "${MediaStore.Files.FileColumns.DISPLAY_NAME} ASC" + } + + context.contentResolver.query( + collection, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME) + val typeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) + val dateModifiedColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + val name = cursor.getString(nameColumn) + val type = when (cursor.getInt(typeColumn)) { + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaType.Video + MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO -> MediaType.Audio + else -> MediaType.Image + } + val path = cursor.getString(pathColumn) + val dateModified = cursor.getLong(dateModifiedColumn).seconds.inWholeMilliseconds + val contentUri = ContentUris.withAppendedId(collection, id) + + mediaList.add(MediaItem(id, contentUri, name, type, path, dateModified)) + } + } + mediaList + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaViewModel.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaViewModel.kt new file mode 100644 index 0000000000..b5888356da --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/browse/MediaViewModel.kt @@ -0,0 +1,57 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// Generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv.feature.browse + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import net.mediaarea.mediainfo.tv.feature.settings.SettingsRepository + +class MediaViewModel( + private val repository: MediaRepository, + private val settingsRepository: SettingsRepository +) : ViewModel() { + + private var loadJob: Job? = null + private val _uiState = MutableStateFlow(MediaUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadMedia() { + // Check if the job is already active; if so, do nothing + // Prevent accidental parallel runs that are unnecessary and may cause race conditions + if (loadJob?.isActive == true) return + + loadJob = viewModelScope.launch { + _uiState.value = MediaUiState.Loading + @Suppress("TooGenericExceptionCaught") + try { + val items = repository.fetchMedia(settingsRepository.settingsFlow.first().sortOrder) + if (items.isEmpty()) { + _uiState.value = MediaUiState.Empty + } else { + _uiState.value = MediaUiState.Success(items) + } + } catch (e: Exception) { + _uiState.value = MediaUiState.Error(e.message ?: "Unknown Error") + } + } + } +} + +sealed class MediaUiState { + object Loading : MediaUiState() + object Empty : MediaUiState() + data class Success(val items: List) : MediaUiState() + data class Error(val message: String) : MediaUiState() +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportActivity.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportActivity.kt new file mode 100644 index 0000000000..ee9288574e --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportActivity.kt @@ -0,0 +1,204 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.feature.report + +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.KeyEvent +import android.webkit.WebView +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import androidx.tv.material3.surfaceColorAtElevation +import net.mediaarea.mediainfo.tv.MediaInfoApplication +import net.mediaarea.mediainfo.tv.R +import net.mediaarea.mediainfo.tv.ui.theme.AndroidTheme + +// Extend from AppCompatActivity instead of ComponentActivity for WebView dark mode +class ReportActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + ReportView(intent) + } + } +} + +private fun Context.findActivity(): ComponentActivity? = when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null +} + +@Composable +private fun ReportView(intent: Intent) { + val context = LocalContext.current + val app = context.applicationContext as MediaInfoApplication + val viewModel: ReportViewModel = viewModel(factory = app.factory) + val isDark by viewModel.darkModeEnabled.collectAsStateWithLifecycle() + val fileName: String? = intent.getStringExtra("FILE_NAME") + val toastMessage = stringResource(R.string.please_wait) + val isLoading = viewModel.isLoading + val report = viewModel.report + + // Prevent leaving activity while loading + // since cancelling MediaInfo's parsing is not implemented + BackHandler(enabled = isLoading) { + Toast.makeText( + context, + toastMessage, + Toast.LENGTH_SHORT + ).show() + } + + // Parsing + LaunchedEffect(Unit) { + val uri: Uri? = intent.data + val completeName: String? = intent.getStringExtra("COMPLETE_NAME") + viewModel.generateReport(uri, completeName) + } + + // Report display + AndroidTheme(isInDarkTheme = isDark) { + Report(isLoading, report, fileName) + } +} + +@Composable +private fun Report(isLoading: Boolean, report: String?, fileName: String?) { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + Column { + Box( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(0.5.dp)) + .fillMaxWidth() + .padding(5.dp), + contentAlignment = Alignment.Center + ) { + Text(stringResource(R.string.app_name) + " - " + fileName) + } + if (isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } else { + if (report == null) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text("Something unexpected has occurred.") + } + } else { + WebViewComposable(report) + } + } + } + } +} + +@Composable +private fun WebViewComposable(report: String) { + val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + AndroidView( + modifier = Modifier + .fillMaxSize() + .focusable() + .focusRequester(focusRequester) + .onKeyEvent { keyEvent -> + // Finish the activity immediately on back pressed + // Without this, it will require two back presses to exit report activity + // because the first will remove focus from WebView and only the second will go back + if (keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_BACK && + keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP + ) { + + context.findActivity()?.finish() + + return@onKeyEvent true + } + false // Let other handlers deal with it + }, + factory = { ctx -> + WebView(ctx).apply { + // use loadDataWithBaseURL instead of loadData to prevent issues from special characters + loadDataWithBaseURL(null, report, "text/html", "utf-8", null) + } + } + ) + // Get focus on WebView immediately on launch + // Without this, it will require an enter press before + // the WebView gains focus and can be scrolled with arrow keys + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun ReportPreviewLoading() { + AndroidTheme(isInDarkTheme = true) { + Report(true, null, "Test.mkv") + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun ReportPreviewLoaded() { + AndroidTheme(isInDarkTheme = true) { + Report(false, "", "Test.mkv") + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun ReportPreviewUnexpected() { + AndroidTheme(isInDarkTheme = true) { + Report(false, null, "Test.mkv") + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportViewModel.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportViewModel.kt new file mode 100644 index 0000000000..97e4780712 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/report/ReportViewModel.kt @@ -0,0 +1,79 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.feature.report + +import android.content.ContentResolver +import android.net.Uri +import android.os.ParcelFileDescriptor +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mediaarea.mediainfo.tv.Core +import net.mediaarea.mediainfo.tv.feature.settings.FLOW_TIMEOUT +import net.mediaarea.mediainfo.tv.feature.settings.SettingsRepository + +class ReportViewModel( + private val contentResolver: ContentResolver, + private val repository: SettingsRepository +) : ViewModel() { + + private var wasTriggered = false + var isLoading by mutableStateOf(true) + var report: String? by mutableStateOf(null) + + val darkModeEnabled: StateFlow = repository.settingsFlow + .map { it.darkModeEnabled } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(FLOW_TIMEOUT), + initialValue = true + ) + + fun generateReport( + uri: Uri?, + completeName: String?, + dispatcher: CoroutineDispatcher = Dispatchers.IO + ) { + // Prevent parallel/repeated execution + if (wasTriggered) return + + wasTriggered = true + + viewModelScope.launch(dispatcher) { + val settingsFlowFirst = repository.settingsFlow.first() + val legacyStreamDisplayEnabled = settingsFlowFirst.legacyStreamDisplayEnabled + val displayCaptions = settingsFlowFirst.displayCaptions + var fd: ParcelFileDescriptor? = null + if (uri != null) { + try { + fd = contentResolver.openFileDescriptor(uri, "r") + } catch (_: Exception) { + } + } + if (fd != null && completeName != null) { + report = + Core.generateReport( + fd.detachFd(), + completeName, + legacyStreamDisplayEnabled, + displayCaptions + ) + } + isLoading = false + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/Settings.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/Settings.kt new file mode 100644 index 0000000000..82e129b476 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/Settings.kt @@ -0,0 +1,149 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.feature.settings + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import net.mediaarea.mediainfo.tv.MediaInfoApplication +import net.mediaarea.mediainfo.tv.R +import net.mediaarea.mediainfo.tv.ui.components.CategoryHeader +import net.mediaarea.mediainfo.tv.ui.components.MultiOptionListItem +import net.mediaarea.mediainfo.tv.ui.components.ToggleListItem +import net.mediaarea.mediainfo.tv.ui.theme.AndroidTheme + +@Composable +fun Settings() { + val app = LocalContext.current.applicationContext as MediaInfoApplication + val viewModel: SettingsViewModel = viewModel(factory = app.factory) + val state by viewModel.uiState.collectAsStateWithLifecycle() + SettingsContent( + state = state, + onEvent = { viewModel.onEvent(it) } + ) +} + +@Composable +private fun SettingsContent( + state: MediaInfoSettings, + onEvent: (SettingsEvent) -> Unit +) { + LazyColumn(contentPadding = PaddingValues(32.dp)) { + item { + Text( + modifier = Modifier.padding(bottom = 15.dp), + text = stringResource(R.string.settings), + style = MaterialTheme.typography.headlineMedium, + ) + } + item { + CategoryHeader(stringResource(R.string.header_appearance)) + } + item { + ToggleListItem( + title = stringResource(R.string.dark_mode), + description = if (state.darkModeEnabled) + stringResource(R.string.enabled) + else + stringResource(R.string.disabled), + isChecked = state.darkModeEnabled, + onToggle = { onEvent(SettingsEvent.ToggleDarkMode(it)) } + ) + } + item { + val optionsLabels = SortOrder.entries.toTypedArray().map { stringResource(it.labelRes) } + val selectedIndex = SortOrder.entries.toTypedArray().indexOf(state.sortOrder) + MultiOptionListItem( + label = stringResource(R.string.file_sort_order), + supportingText = stringResource(R.string.file_sort_order_desc), + options = optionsLabels, + selectedIndex = selectedIndex, + onOptionChanged = { onEvent(SettingsEvent.CycleSortOrder) } + ) + } + item { + CategoryHeader(stringResource(R.string.header_mediainfolib)) + } + item { + ToggleListItem( + title = stringResource(R.string.legacy_stream_display), + description = stringResource(R.string.legacy_stream_display_desc), + isChecked = state.legacyStreamDisplayEnabled, + onToggle = { onEvent(SettingsEvent.ToggleLegacyStreamDisplay(it)) } + ) + } + item { + val options = DisplayCaptions.entries.toTypedArray().map { stringResource(it.labelRes) } + val selectedIdx = DisplayCaptions.entries.toTypedArray().indexOf(state.displayCaptions) + MultiOptionListItem( + label = stringResource(R.string.display_captions), + supportingText = stringResource(R.string.display_captions_desc), + options = options, + selectedIndex = selectedIdx, + onOptionChanged = { onEvent(SettingsEvent.CycleDisplayCaptions) } + ) + } + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun SettingsContentPreviewDark() { + val isDark = true + AndroidTheme(isDark) { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + SettingsContent( + state = MediaInfoSettings( + darkModeEnabled = isDark, + sortOrder = SortOrder.DATE, + legacyStreamDisplayEnabled = false, + displayCaptions = DisplayCaptions.COMMAND + ), + onEvent = {} // Empty lambda for preview + ) + } + } +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun SettingsContentPreviewLight() { + val isDark = false + AndroidTheme(isDark) { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + SettingsContent( + state = MediaInfoSettings( + darkModeEnabled = isDark, + sortOrder = SortOrder.DATE, + legacyStreamDisplayEnabled = true, + displayCaptions = DisplayCaptions.COMMAND + ), + onEvent = {} // Empty lambda for preview + ) + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsRepository.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsRepository.kt new file mode 100644 index 0000000000..88d3a932e6 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsRepository.kt @@ -0,0 +1,92 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// From code generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv.feature.settings + +import android.content.Context +import androidx.annotation.StringRes +import androidx.datastore.core.IOException +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import net.mediaarea.mediainfo.tv.R + +private val Context.dataStore by preferencesDataStore(name = "settings") + +data class MediaInfoSettings( + val darkModeEnabled: Boolean, + val legacyStreamDisplayEnabled: Boolean, + val sortOrder: SortOrder, + val displayCaptions: DisplayCaptions, +) + +enum class SortOrder(val key: String, @get:StringRes val labelRes: Int) { + DATE("date_modified", R.string.date_modified), + PATH("file_path", R.string.file_path), + NAME("file_name", R.string.file_name); + + companion object { + fun fromKey(key: String?) = entries.find { it.key == key } ?: DATE + } +} + +enum class DisplayCaptions(val key: String, @get:StringRes val labelRes: Int) { + CONTENT("Content", R.string.display_captions_content), + COMMAND("Command", R.string.display_captions_command), + STREAM("Stream", R.string.display_captions_stream); + + companion object { + fun fromKey(key: String?) = entries.find { it.key == key } ?: COMMAND + } +} + +class SettingsRepository(private val context: Context) { + private companion object { + val darkModeKey = booleanPreferencesKey("dark_mode") + val legacyStreamDisplayKey = booleanPreferencesKey("legacy_stream_display") + val sortOrderKey = stringPreferencesKey("file_sort_order") + val displayCaptionsKey = stringPreferencesKey("display_captions") + } + + val settingsFlow: Flow = context.dataStore.data + .catch { exception -> + if (exception is IOException) emit(emptyPreferences()) else throw exception + } + .map { prefs -> + MediaInfoSettings( + darkModeEnabled = prefs[darkModeKey] ?: true, + legacyStreamDisplayEnabled = prefs[legacyStreamDisplayKey] ?: false, + sortOrder = SortOrder.fromKey(prefs[sortOrderKey]), + displayCaptions = DisplayCaptions.fromKey(prefs[displayCaptionsKey]) + ) + } + + suspend fun updateSetting(key: Preferences.Key, value: T) { + context.dataStore.edit { prefs -> + prefs[key] = value + } + } + + suspend fun updateDarkMode(enabled: Boolean) = + updateSetting(darkModeKey, enabled) + + suspend fun updateLegacyStreamDisplay(enabled: Boolean) = + updateSetting(legacyStreamDisplayKey, enabled) + + suspend fun updateSortOrder(sortOrder: SortOrder) = + updateSetting(sortOrderKey, sortOrder.key) + + suspend fun updateDisplayCaptions(displayCaptions: DisplayCaptions) = + updateSetting(displayCaptionsKey, displayCaptions.key) +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsViewModel.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsViewModel.kt new file mode 100644 index 0000000000..a2863d3426 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/feature/settings/SettingsViewModel.kt @@ -0,0 +1,70 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// From code generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv.feature.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +const val FLOW_TIMEOUT = 5000L + +class SettingsViewModel(private val repository: SettingsRepository) : ViewModel() { + + val uiState: StateFlow = repository.settingsFlow + .stateIn( + scope = viewModelScope, + // WhileSubscribed(5000) keeps the flow active for 5s after the UI stops listening + // (handles configuration changes like rotation) + started = SharingStarted.WhileSubscribed(FLOW_TIMEOUT), + initialValue = MediaInfoSettings( + darkModeEnabled = true, + sortOrder = SortOrder.fromKey(null), + legacyStreamDisplayEnabled = false, + displayCaptions = DisplayCaptions.fromKey(null) + ) + ) + + fun onEvent(event: SettingsEvent) { + viewModelScope.launch { + when (event) { + is SettingsEvent.CycleSortOrder -> { + val allOptions = SortOrder.entries.toTypedArray() + val currentIndex = allOptions.indexOf(uiState.value.sortOrder) + val nextIndex = (currentIndex + 1) % allOptions.size + repository.updateSortOrder(allOptions[nextIndex]) + } + + is SettingsEvent.CycleDisplayCaptions -> { + val allOptions = DisplayCaptions.entries.toTypedArray() + val currentIndex = allOptions.indexOf(uiState.value.displayCaptions) + val nextIndex = (currentIndex + 1) % allOptions.size + repository.updateDisplayCaptions(allOptions[nextIndex]) + } + + is SettingsEvent.ToggleDarkMode -> { + repository.updateDarkMode(event.enabled) + } + + is SettingsEvent.ToggleLegacyStreamDisplay -> { + repository.updateLegacyStreamDisplay(event.enabled) + } + } + } + } +} + +sealed class SettingsEvent { + object CycleSortOrder : SettingsEvent() + object CycleDisplayCaptions : SettingsEvent() + data class ToggleDarkMode(val enabled: Boolean) : SettingsEvent() + data class ToggleLegacyStreamDisplay(val enabled: Boolean) : SettingsEvent() +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/CategoryHeader.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/CategoryHeader.kt new file mode 100644 index 0000000000..5328b3fdbf --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/CategoryHeader.kt @@ -0,0 +1,79 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// Generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import androidx.tv.material3.darkColorScheme + +/** + * A section header for TV settings groups. + * Includes built-in top spacing to visually separate it from the preceding category. + * + * @param title The text to display as the category name. + * @param modifier Modifier for the root [Column]. + */ +@Composable +fun CategoryHeader( + title: String, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .padding(horizontal = 16.dp) + .padding(top = 24.dp, bottom = 8.dp) // Built-in spacing + ) { + Text( + text = title.uppercase(), // Uppercase often helps distinguish headers on TV + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + letterSpacing = 1.2.sp, + modifier = Modifier.alpha(0.6f) + ) + } +} + +@Preview( + name = "Full Screen TV Settings (Dark 4K)", + device = "id:tv_4k" +) +@Composable +private fun CategoryHeaderDarkPreview() { + MaterialTheme(colorScheme = darkColorScheme()) { + Surface(modifier = Modifier.fillMaxSize()) { + Column { + CategoryHeader(title = "Network & Internet") + + Text( + text = "Wi-Fi: Connected", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.bodyLarge + ) + + CategoryHeader(title = "System Update") + + Text( + text = "Check for updates", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.bodyLarge + ) + } + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MediaListItem.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MediaListItem.kt new file mode 100644 index 0000000000..e25f668945 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MediaListItem.kt @@ -0,0 +1,105 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Icon +import androidx.tv.material3.ListItem +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import androidx.tv.material3.darkColorScheme +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * A TV ListItem for displaying media items. + * + * @param name The headlineContent of the item such as the file name. + * @param icon An ImageVector to be used as the leadingContent. + * @param path The path of the item shown as supportingContent. + * @param date The date of the item as the number of milliseconds since January 1, 1970, 00:00:00 GMT. + * @param onClick Function to be executed when the item is clicked. + */ +@Composable +fun MediaListItem( + name: String, + icon: ImageVector, + path: String, + date: Long, + onClick: () -> Unit +) { + // ListItem automatically animates scale/color on focus + ListItem( + selected = false, + onClick = onClick, + headlineContent = { + Text(text = name, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + supportingContent = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = path, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + .format(Date(date)), + modifier = Modifier.padding(start = 5.dp), + maxLines = 1 + ) + } + }, + leadingContent = { + Icon(imageVector = icon, contentDescription = null) + } + ) +} + +@Preview(showBackground = true, device = "id:tv_4k") +@Composable +private fun MediaListItemPreview() { + MaterialTheme(colorScheme = darkColorScheme()) { + Surface { + LazyColumn( + contentPadding = PaddingValues(32.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + MediaListItem( + name = "Test audio.flac", + icon = Icons.Default.MusicNote, + path = "/storage/emulated/0/Music/Recordings/Test audio.flac", + date = 1767856522000L, + onClick = {} + ) + } + } + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MultiOptionListItem.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MultiOptionListItem.kt new file mode 100644 index 0000000000..ffc7d2420d --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/MultiOptionListItem.kt @@ -0,0 +1,154 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// Generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronLeft +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Icon +import androidx.tv.material3.ListItem +import androidx.tv.material3.LocalContentColor +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import androidx.tv.material3.darkColorScheme + +/** + * A TV-optimized settings row that cycles through multiple options. + * + * @param label The primary title of the setting. + * @param options A list of strings representing the available choices. + * @param selectedIndex The index of the currently active option. + * @param onOptionChanged Callback triggered with the new index when the user clicks. + * @param modifier Modifier for the root [ListItem]. + * @param supportingText Optional description string displayed below the label. + */ +@Suppress("LongParameterList") +@Composable +fun MultiOptionListItem( + label: String, + options: List, + selectedIndex: Int, + onOptionChanged: (Int) -> Unit, + modifier: Modifier = Modifier, + supportingText: String? = null +) { + var isFocused by remember { mutableStateOf(false) } + ListItem( + selected = false, + onClick = { + val nextIndex = (selectedIndex + 1) % options.size + onOptionChanged(nextIndex) + }, + headlineContent = { + Text(text = label) + }, + supportingContent = supportingText?.let { + { + Text( + text = it + ) + } + }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Use LocalContentColor for the arrows (they will naturally dim/brighten) + val iconColor = LocalContentColor.current.copy(alpha = 0.6f) + + Icon( + imageVector = Icons.Default.ChevronLeft, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = iconColor + ) + + Text( + text = options[selectedIndex], + style = MaterialTheme.typography.labelLarge, + color = if (isFocused) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.primary + } + ) + + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = iconColor + ) + } + }, + modifier = modifier.onFocusChanged { isFocused = it.isFocused } + ) +} + +@Preview( + name = "MultiOptionListItem 4K Static", + device = "id:tv_4k", +) +@Composable +private fun MultiOptionListItemStaticPreview() { + MaterialTheme(colorScheme = darkColorScheme()) { + Surface( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.padding(32.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Example 1: Standard usage + MultiOptionListItem( + label = "Screen Resolution", + options = listOf("1080p", "4K (Recommended)", "720p"), + selectedIndex = 1, + onOptionChanged = {} + ) + + // Example 2: With supporting text + MultiOptionListItem( + label = "Language", + options = listOf("English", "Español", "Français"), + selectedIndex = 0, + onOptionChanged = {}, + supportingText = "Choose your preferred system language" + ) + + // Example 3: Long labels to check text wrapping/truncation + MultiOptionListItem( + label = "High Dynamic Range (HDR) Display Mode", + options = listOf("Always On", "Adaptive", "Off"), + selectedIndex = 1, + onOptionChanged = {}, + supportingText = "HDR improves the range of color and contrast" + ) + } + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/ToggleListItem.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/ToggleListItem.kt new file mode 100644 index 0000000000..57b2e02221 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/components/ToggleListItem.kt @@ -0,0 +1,108 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +// Generated by Google Gemini 3 Flash + +package net.mediaarea.mediainfo.tv.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.ListItem +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Switch +import androidx.tv.material3.Text +import androidx.tv.material3.darkColorScheme + +/** + * A specialized list item for TV interfaces that displays a title, + * description, and a trailing [Switch]. + * + * This component is optimized for D-pad navigation. By handling the + * click at the [ListItem] level and setting the Switch's `onCheckedChange` + * to null, the entire row becomes a single focusable target, preventing + * "double-focus" issues common on TV platforms. + * + * @param title The primary text to be displayed in the headline. + * @param description The secondary text providing additional context. + * @param isChecked Whether the switch is currently in the 'on' or 'off' state. + * @param onToggle Lambda invoked when the list item is clicked, + * returning the new desired state of the toggle. + */ +@Composable +fun ToggleListItem( + title: String, + description: String, + isChecked: Boolean, + onToggle: (Boolean) -> Unit +) { + ListItem( + selected = false, + onClick = { onToggle(!isChecked) }, + headlineContent = { Text(title) }, + supportingContent = { Text(description) }, + trailingContent = { + Switch( + checked = isChecked, + onCheckedChange = null // Critical for TV: the Row captures the click + ) + } + ) +} + +@Preview( + name = "ToggleListItem 4K Gallery", + device = "id:tv_4k" +) +@Composable +private fun ToggleListItemPreview() { + MaterialTheme(colorScheme = darkColorScheme()) { + Surface( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.padding(32.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Picture & Inputs", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 24.dp) + ) + + // Example 1: Standard TV Toggle + ToggleListItem( + title = "Game Mode", + description = "Bypasses post-processing to reduce input lag for consoles.", + isChecked = true, + onToggle = {} + ) + + // Example 2: HDMI specific setting + ToggleListItem( + title = "HDMI-CEC", + description = "Allow the TV to control external devices over HDMI.", + isChecked = false, + onToggle = {} + ) + + // Example 3: Connectivity/Privacy setting + ToggleListItem( + title = "Wake on LAN", + description = "Allows the TV to be turned on from standby via the network.", + isChecked = true, + onToggle = {} + ) + } + } + } +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Color.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Color.kt new file mode 100644 index 0000000000..5b50c12980 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Color.kt @@ -0,0 +1,17 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Theme.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Theme.kt new file mode 100644 index 0000000000..1cad53723b --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Theme.kt @@ -0,0 +1,47 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.ui.theme + +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.darkColorScheme +import androidx.tv.material3.lightColorScheme + +@Composable +fun AndroidTheme( + isInDarkTheme: Boolean = true, + content: @Composable () -> Unit, +) { + // Set XML views theme which is used by WebView + LaunchedEffect(isInDarkTheme) { + if (isInDarkTheme) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + } + val colorScheme = if (isInDarkTheme) { + darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 + ) + } else { + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + ) + } + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Type.kt b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Type.kt new file mode 100644 index 0000000000..9dab1888b0 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/java/net/mediaarea/mediainfo/tv/ui/theme/Type.kt @@ -0,0 +1,42 @@ +/* Copyright (c) MediaArea.net SARL. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license that can + * be found in the License.html file in the root of the source tree. + */ + +package net.mediaarea.mediainfo.tv.ui.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.Typography + +// Set of Material typography styles to start with +@OptIn(ExperimentalTvMaterial3Api::class) +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) diff --git a/Source/GUI/Android/tvapp/src/main/res/drawable/ic_action_add.xml b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_action_add.xml new file mode 100644 index 0000000000..62c4bb67f7 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_action_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/Source/GUI/Android/tvapp/src/main/res/drawable/ic_banner_foreground.xml b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_banner_foreground.xml new file mode 100644 index 0000000000..b6ef7e8e62 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_banner_foreground.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_foreground.xml b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..240356ce63 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_monochrome.xml b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000000..eece32e415 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_export.xml b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_export.xml new file mode 100644 index 0000000000..67890c4dec --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_export.xml @@ -0,0 +1,10 @@ + + + diff --git a/Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_view.xml b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_view.xml new file mode 100644 index 0000000000..38cbe99dec --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_menu_view.xml @@ -0,0 +1,10 @@ + + + diff --git a/Source/GUI/Android/tvapp/src/main/res/drawable/ic_report_close.xml b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_report_close.xml new file mode 100644 index 0000000000..b60298d1fe --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/drawable/ic_report_close.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_banner.xml new file mode 100644 index 0000000000..a0a0dece93 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_banner.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..5ed0a2df70 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..7353dbd1fd --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml new file mode 100644 index 0000000000..10bc7a5de4 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml new file mode 100644 index 0000000000..1084c24082 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/mipmap-anydpi-v33/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-hdpi/ic_launcher.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..534f2ded0f45cc714394b88baf55fa1c84de964a GIT binary patch literal 1084 zcmV-C1jGAMNk&FA1ONb6MM6+kP&iB|1ONapN5Byfwa|hj+q7-XYumPM+qP|+&;GJ) z+qTVX+cvh2*V?hh)#p6!zV_AynnrMP`HE|hLeN2w$%q72Mv){bN{<_>YhUXH4{Y2? zAPob>7663F+}*7%`3*LRJR+IS6;??u$z9UM8SDUh9s7>lNKzz6pN4Va3;M{rAc8?e zM2dAo5FstpWp|Dvh?O8VgV+yZH;6q;^D#Ecd|cK+RDyVR;gl2Bcgn@{4k{)ih@jRk zUR-eSBhslIyeUFD*2r#HDs*EuvA8N;|t8h zv-k@llK#-(T>j|f>Di>KU+p^4Fb6i_Ep)*hScn4vNp8TMD2HpY90vs2@FxnPAYR8l z8~{kpK}jUV7Tz;JzsG(dZ-gP>J-4dXB#vta{Casm6{X4ZorXkO2!pGO7=NXF#q+Spbz`x&A;x+<=m(g&zP+ z21!l<;30RRHh`qCTsixA%H}%Tp9ql8FvFfW9$IQhXHu^ zgFG#v1_0DVZU7hxlDrncIFS2L4-%e35&-^M0PqFE7U>7zL6DnK1wdg~ZWo#%A&?x2 z0ZhYwkX(=QKvHZ3`4=LA;jsSsUx$e(goH?h61W|^K$7tZa4OOc@&f>uo`l5vxD+44 zy9VLk$2LGNECa~_0J38{L_Z%MLqcT0ry#i+MWJwNIw;72?~xf<@g_*}5dgWc7?Pi# z4<=OBH3ZNi9zZ=q;1<-=R4P5>rZVP)9#{+Fc+Z>*c#d#*|u%lZolgP)01hH zxQ=AowC0{`8&TZ9mwzg@t^MwC9Lct-4DJp)_ee;9I9U^s>vxyqMz*%+nEBg)KC={y zu?}M)Ia6hu@V7>(vVfc&Ms6c1a-D~l;R8Eh{QdXs3p80G`I3k}L_`kk&!@@$=sm-u z)75)*IaAZ1P}8_m^WQY)pZ+=sPJ$A3Tqf*!aA8Cp>V=5EK^{cDjQ8KuE zRa-PFgDx4U4I;XTQK)I`00okC7S2?oOYT`F85`<>h=`)5agfIDlICEQkG?uA9|t~| z-6b^a3-HBe!6YZM5V{Fp0n#Hin1k7cSlEpO>y;te&|G%Wq<8^#ebz^7FuVA+A;GTa zipdRTNoe#7?4q3G49#UB-rj)S_1grMx$H7^8~nl*_JdgrXO7~RY7%oo1%QW&>Ij8Lc+ zb7nM%VL%z}wPKham*N&q=QG+Otn~xwsjUj|9&05EQHn!kTHip!(C;l+f9w-S@#$x* zDNrVfHNTtCa{s}i+?488w7Ys=m5v#81iG9NCCK?CFi+Q1S1h!wfE@~`q9djOPO z21j_uAa{%14Hudx$f@|3W17RHVr2{k?vow>+=0^mtB5!vD}>M<(6FT%x6L8D%&TDj z*2NW2KI~q_Jdd(R;lc{20b+ve_P^#yZYrYuk}W$2&M+Qd18W2CDSsc4E&hQND7W}O z5_Ca6NCja*$kt78mP7Qd?hX`wV+4{DvtOnh6u#HC zfm_4#00i$jQn~2dLX-gb90zX~rt3F@Z@?|F@5`Jigm=gMu?2xL4^o`!EdOi}?8j0z`_ND{VUm04VUgj+|dg5Nrl_nn?!08B#CA zRz?Ckz6IWX7!O3zI4`-?1t)IAqxk|L%PL*gFzG{pq-^Rx_KNs|S7mvC>tkwLK1>Fv zVgIS~H+*2>o&L9Oa?^$25d*C~4BIZ)Rko&qqj&`WFlFEpX78qodlwEsB)5K1nZj`F zf^}tQYJ$CK$mP~Bd>BBWwEG`sK?neAh%@P#?Xz!r-GJZ)sSe(RAO?VC+Yop2L_{&; zDFjMR_oHVg0B{%^;@4>T7B$T_5bOp9%fthKN799ww#VUhIx1eSOoPhMr|rhL*DQon zv}H=bd)QC>!oivkpugn0^N~M+l{b%z0M6Qt+_q-*WA8K@55FqT!5XC zQyIeUhsvQ_|H!U7Gm`p8B>ZU8KU)6201ywt0NQgOQ%kA~OH@2$e;mXE;3_4oHe zQ5~eD{n$?z@coelfC|72;LiuJ=MCM*I-GwRqqH{gj#7is7=Qqv3NU920XzW`fEqyf zP;r6&oxH@3!`2=JmP0*8vS@faG!rPLu`--KKjtQJ1lCR+smL&> z+#}r|Wmih#L_b!BgnNM|SbsW7CLk(ThNu7Gr?L9uaW=FF75UhVW&u=2q4qG&p?u`6 z@j#FBIAhF(X@Os=qY+d(`p+lYa?U3zyq&0e5GEjKc1QKhR6M+AbxKf`I9PK8&K~pH zR?)uJhhiAdwwZWyB0W3O1ECLV12locYXVqnL{|9^7UhPVmzIj2_l3WrTdz^UGUOdV z`E9ot3NLknYBq&30q+Z4l=XfhJhn;RYo|gqqHv(=Iv?Uh!nK$&rA`z*N@%YY!>Wn4 zo7iKmH?y&>pkX-Hnmskha^eV^3NOFF!ABmjsW9=X%pY@_YyGV|Wpxj0*i>kjAv)*q zU{j%eW`GFGsKBOz>FhZLrE;#d^vuuSaGC8~dph`fdkoYVqYRr0NzHzPU6eB%=h_!T zU^^1*da8)*T>G=6dkJ=Z(nE8uJ#MV*MS}Ix0L`)XxIvhvd;>@iRbtGwPmsu{ufS&; z1(Ja6P{?qy+DBg;kcSO#xaXHDBasH2nP!*WwImU<$(~;#8jAimEvR2Z{f?5w<)_A? y5otuE`|O(rO`b@;C88e}HI0{G{(1sNMxB4pNmBgnJ?GAuAvFj#VEp~}ePjVnFydYS literal 0 HcmV?d00001 diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..daba21594f525b6ac171a5c32b8166a53de8de92 GIT binary patch literal 2328 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 literal 0 HcmV?d00001 diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..d4846938bf7b3ea81f85d434d818671a7cf19404 GIT binary patch literal 1306 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 Av;Y7A literal 0 HcmV?d00001 diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..fbe1a1c57271212afd13ea5131e841a8f57e9be6 GIT binary patch literal 3120 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 literal 0 HcmV?d00001 diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..ed4f00c2714a17f1126181f144e95b13a99a9d2e GIT binary patch literal 5014 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 literal 0 HcmV?d00001 diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..7710ff6cdfd35c14c00003b1c90fc2c6208f9be8 GIT binary patch literal 5120 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 MsWl~CjX?UUi-cXdO8@`> literal 0 HcmV?d00001 diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..c5935bbce79c13e769ab19197b5ab1b29a9e1d11 GIT binary patch literal 7260 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 literal 0 HcmV?d00001 diff --git a/Source/GUI/Android/tvapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Source/GUI/Android/tvapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..33906d3be22c2c8f1765f218c89a5a482c7db9d1 GIT binary patch literal 7540 zcmV-)9gE^pNk&F&9RL7VMM6+kP&iCq9RL6?zrZgL6^GikZ5a9gx6{iW5enP3ZL)XL zp22Tah7F^Ba3_=n2htVBBt{xjF;D^%9|}_SnO za_r$3vMw6Q3z0P|D~gq)i5jOA7ZM$K)uxKo zY^7-QRW5kvCe3y=6RjN0LblrM6jF#zmQwKfqQwXa)V3XObGF!Nu&iN`HFj;ywr$(C zZQHhO+g54^#sB}0MA>%Zwz&X@(8D8?u5#87p#J}#X`PYz|!2Ic%8;vT`NZCiCUyMTw(yz%UvP559090hNaibhA*lT}N{ zlS1t;zzEXrW$SDe%!E7VQs?R!2~Um=V?6hwUskbkwAaXS7oGxdV2ta;6{dtzjIryK z3pvi{W!oGmgoEKL(ytRzdWOR`owQGZL!gLFV^oCW4?hmy6Mk4zsz)Mhq<#L!|B&NY z@;``E#i;~c;4_*Em7)&g7}#0GshsRk4}q77Dw9$nROkpp)FCVoi3%#yVHL&;t$Oq# z2jjP={{_bxQC9`s;ZKYu%|U4l#y@32T6A(8)WO3TXYzVv!=uAE>12*`qH7a$f?i3d z&^&b5B1By(C-*+MQPL@N55Z0E{)vKbZnh?x}OZ8G;$&O7@P@7J*&c# zX$^fo!hlMuj9i!p{vc%9i3<-|v;AXlsx&_Uti?um1IIOMP$nXl!_-D#u zagO&5YRG6B-4om}m$KNL`UCTN$GeS8K5yVR3Dej)Oo07=bT@KLrIR}#MD&!hSf6{p zXf9{yMWmN=i;k0B_Q%2yj5-ZyoR^6ka&F-gtf|vS0@ySYFl4oxQ!z0?&>LfoK2ive z^*{_5;x;m{Cd9x}oT-l_LPRL`DeIan^K^0pgreutn8r$)n<#M~TW{S+^*qR7p{eTI zu#A{RkKVMlKE`Bt`lc)TAl4X|{irCDQ#JdV6Q!^1z~#ml#I z=%HKEltQ`nlmEBhK2UlWndT!&lOCD&HLNxIqgl$O0a~PKI%612!^=1f-04=y)HBl^ z+CeH)2V*|&5l_JOLGrVwC=iB5rb}9)Byz%DHvKawqhV~FebBv^d!{Tk_r7Y;AG}a6 znXThNq*!b;wy!S+gr%FCr)2yQoC2wZficpAVc7rOBHT^rGQ1b4E!c+_S|dnG#hRg( z7!jWA15zug|QY4r&~wN*&k@=y2%>f6EMaM}{oUxGjRy zgoZ9eR!sy#+qu4JM#cc=)8{gvyHzsiTPC)}xD;u~*&ZpO6<=gdZ`jeBj6$29mWV(7 zz3yO+-~q9t4N>1@g3=U_>X-!Ee4y{M2MXNYh;oKyQXgVR9vYPr@QGGDk^wz|Y^$(6_C9y5-`P<3;lx{y?mh8i*FZ1Y->WdjB88mEP<@mW>Jhj zKiS0WWEh(CvRoOVXO1uY9I>+nnxrf|!IogO52hK5Zr1_Rt%^v0OcJcZB9cAO<=Mfa z!gB?p^H&V2Zrm^;LoP`QS%)+AEQ0zYWw;WOP9Zd9?u&N2lP-xszlTQn8|zpF-4PV7 z1f~=4I(}Q<|1XNNypSO^N#h1!Y(oh%)*dnYXJ{;aAueiHeCJ_Kd^4JF_oo7yFPs^WM{ z%G8UEE1sC%iBvRW&}^YkI&)BM%MCc;H?feAJy`0NTuBUVawwgCka|p0@TX3{1go;K z%cCz4T8@_P`uk&uYgD0SY9%a6sYe1)Gn-3r;G}$+aDObqZgYHRNOcXEp=Yp8k1j<0!w%3{Hgu3kF#+QX zq*Iw!w7*_xIa=npLxk`T;?Tn>qjjNnDTw2Ri>UDd^UyLl7m9j&pJtQh@(kaob{Kom zhWa8I*cm!Az9512S%7DksgRPeLr}k;Px5OoRS+F8$1lPfJ>rmSQYc0#(Ss(s>d)z9xF2 zgFaLgoR=&NcbG3y0U$M|M7glEr}qgCpTtHfU<_)g!9t1!3_=>P%u*2qt2h82sRxu! z@gQ>RP9;H13Ajs5`;NmJJqw@~;TEe24utu}13=l`iE*fzyTg1_sawKR=PT(o-GyC+wz&J=!n;gJ9d2ohs z9t2>!YD_SgfHu!QMdFjdd4RE%M(_vg(7>^7DXft;Zv^Ha*Q_a&?vRY5GJ+!vM-Zf` z763|;Y>rZ~hxdVok~=aaI89v=MHSeC0;AI$J3pcT#i)(pkl|PUffWLXdvFw>Zb0#z zeSObs448-0J73NQtf3t-wjn_O| zoB9#+A+XMv8KN~H{y74ptG%(~w!+ua^EnK_8+Q=v80Yv5Q7j8gm_4QQda9hxzYOA> z(&fO|ei_0As_21p5Xd~Y**WDqXt@Ecq-xSt>|vaoQ7n_z#Ln7t>yE6jaZ#f1j#eS? z6b+a^NF2zVf{{Ehw!RO9B~i4q0CQte=o2g;nWjUy`D|I2u^<`^JJJ5Ui>wfn1gZub z$IO!e)P4?;H5f#-dAmj()9Yx7f0Q!p1LHWj43e!3h%SF@=FI33C$*1XSdykMM2zPJ zK1%Nz$aAg%5gClGiaLF}fj*uN_u=s`s2BE|aXMlPmis$^_AeBr)CavoXQhW?RXf7z zk#{m%GzU29Ar3GxatGc1-`g4gS~ndv!8So2<3v|1Nzp1DGu{E(Ytkm39|kaO!!4B}`)`2mfB)a!W$y&6|gYWoC|`3;_wW z0Z+AnD-wu=%jl=T+NbQ%>4g&5IZouWGRz(qE0Kz366%6aIHLX~&HE6iKzOGoFz6q6 zayC7G54bxgih@kjD7y&ojN8MsO!g>V z4nqm#6CN|p@VMz<+CC6b9gpyC^JXYzE9zo?Xw?&_xm1ugy)ehwgBDe z)t8I7+#gcwR`0R^k2D1azq$R{vVZZp>G%=>kPcsldB*7%*-nDeIV4!yg_g} z10!!R8n4+A<);PEZYBhhUgJ>sxXnA?ND+qa(2=*P41#zlARf9IBX4uoE9V2r4Ma(_ z+<*^4T6V=&1U_!&ni3umVhhcL;z@WAQimT9t{|Tci2Omxn+>F%H7xhr1@wG94~Sou zCMZMVd|E7D!~vq)8bW^H0u(;( zYIQ(PtO?+a&zoR0NP=`xEa8|IR)V|)S3&PL|#6LxtDv_QBa+rrUdIv6NMZez*kO=4Z9 zSkeZb9iBfQz%Ly`7#vgY*cAtx=mCt+5rH~A#$yp>V*`!x1sB>2V)K)~07wHT0s^XT zFkZGu`;R5GxPAVzZCkv~>R|!>!1*RmR13ir1LQR06UG(XvwE|hWnUolx#>v;lDpVL zzGDH&0t$dnWm|ehDP4%K>7b+V2t;5Xj?f5H^+YMNW(e z!8F3z03Q*`mEYX{KG0RH%>^`iEb*TrG523YG*IW{@F<4)2Y>`*2(Yc8K zqvMHL^asB*1SlGI5!F~dh|@Dmu>0!`3km25uK+97ra}7NNFU;{L$CNE33-zK;1>w9 zZta}-9T%WYf2_i0CCed}&{O3NU^52iTl{=fi@)ELltnp7Z@g$ZDbS2OfjvkwM>4Q^ z@rE%;MDHxiGwG^CC^h#+{cAw?k&Aq82_ymb=g5Gv^?0k*hAv}rXDHM-_~5A(#GB;? z*!^(6`DnwlpSLLSv*a~NlCYRQQw-`pI(teEnJ@#Vj`T98msZMK-kDDSU@0(*$&Liwn0r?PBKaZe*2*dke-8XUo?6EBD0Pl zJCYiJv#C6-9uRXr`xy#a~6_OoEjrgO1 z^1W`d*R8`<36RXfqE@*=(-Z1{Bqa6pR5J!`o4*- zxP{x2;(S80348qfoVdhwq?D-=hd}TN%G6G4i!`iak=eN|S$cuc(AZzGH}#Z!@>=R} zlR7~2;6?|QVj;;QhUI5&`{PGKgHLZ;e*1Q58!5!DB^s|NzOEAJd!H;4)+kuR&Fjs= ztu=at@T{n2ID)U*ud93u&1s?Q1pE!_I-cKsEP*|2VgI+;#j5S-;|b9!I+70?;@BXx zO8lWI;X0R>05yo}f2&AHu!Ko=WD_7~=f{u`qScK?y(hp}{E_5NQJ{?bnY(CV?qx1^B5Vd>^Gn%BPbDT(kgiN@ zm3L}1IIpMxU92`(Lfgv>7ebeUPuRWiMr|sIUED3*G2jE)`Vqj;c;(-g3MfP|`{4Bi zO8I7;X848h5mKc+@$4<->qVATQ71>JEnh`dxuT=zi}5&Ffh5DSUl5dvnj2S4LIy~Y zQ(f=<3ZBZ9)pD{QF^C~R`c7bYQm>)aMIQ7p`{c~vcpz$Ol*T)2kRcmC39YIhFtq%g zK&qtc>+wCV6(W;&-u0dW1!5$`ERtyxhUNXXFzm0NZ2aqAd?a~!y^HRdqpK4GkgTKy z_11QGeK!`&-MWWMw&i}IA`G(iL02{*+pu@mn2Wh}_RjbGBd^4^29 z*R7|`H%Z*3!`})Jx@y#vdTWm;kmBV_=}W#Z;q4i7%u!(b*yE_CDb;crFn{6)%I^^Q zT6T;_mC&`Mwp&i;l$%t;g5A&nTe7?TIrlPszS#U8aFS0%9yVaL5V0r3>qTY|M zXuM!8Q*jjC%UzjzfIMzZbH>eEwf%dHpoYy8a;7!(es{Z!ACJqTGs)JIq6ND3tm-?q zZ|!As=8Qn2+?#h*uWPH;rquW>ob0jq3gtlI_yhHwI=AwU25XzTX$|vk-Cg}N4wt;p zN2AGjKw*Uogs)*$-@apuzej`ZJjuKJi%S4AQH1ep&9p{YOPzhSE;U*?37 z>`O(dXZE(=zoVUhU3;ooY&vt97<-}rOS5-;ZV#nAtb5wqdSlDiU)0FIjsrv_<{Bu| z8A}u30ZC%)8aDqr2@P)a!4e_CvX_eKUGR##YbW_vx5p;VQc@OknM5XWAi%x(f$lh$ zRjEs8gXbUftA8?;TuH=YI?bsoS=9l~=PYR-7=z1fY*B5x>OF!_Sec=omTDGTjAyRd zv8*}JmhsS`yZ^R_Q69F;cys-~_a2oIXnUW-oGWQDIIO}2bk{Ph?g(&vS;Ot_5-uAX zRAWcH^1+ukS+JUOQMIeWP(m&wQhr6;@qYyD0j}q5Xzd?{-|--?YKCKX|5LXFxPkqT zj*q)SF6~_na-i}opaJ@oY<~5f`!s&n6JwEMLt4%Gz(K(u?%ZGPUt~|qk{3$93<+Us z^2>lKc3I1cj*RQXzIb=*z)%$N$X0q@go}UWIs2~@pAmgTY;9S7jgMUDWPH^uGYE@0 zc7ghC{TjSDXkQd~&|Dh+0Cd6DukJ8NyjPplqOY;1T~R^YQi{d+eM_ zQ9O^7qTuhO1Y0ltKx&L})}+t_x`GftJ-=83FD)_1*er+v*{R z((wpYx~0R=s`{0;Z$H4Fk;mENT)BWxKLatBK~9JNStr)(pZDqEMhw z6`j?lW{b!79^=nA=lwB+DmRGPkSt0F@rty{KvVQN0j>bg{YPzer)97o#-fxb1g2+0 z>I1V|1Ydpk&*ulY7DT_lG7TsQwM4u^RZIa=bG1Ftyqa@?+JDD>FF&(eVGrzuBA&M? z7*lLKuiqEFuxrta&)%WGU%epjNPD$8Dn(31H?Sl?s!LSR)v!M-0A*tkBk01AX~2_Pk}BYQO)hS_I8{+4+WF+xmNB+PO=~md9gL zP=oHbixR))@lblkQRs*E{zhM4cK(rbzN+PXzj_&G-cjZD>RR!NT&yax%yF?GD$u-) zV;(7NnEwAyS@-H|2e&By)!9Eg7U@=8r_u04c*KGyLZhj}c&YK2LFkFr<&D37?8)nI zKX`)-KcUQ<9B7G&jWcJ_SL`aPTG@fUdhy6Ly*WqAI9EpIpa+_bP= zpkx5%{Om7Y@qrK&QAEYk^d$+uxi_ZGetpM4v%V_8{``C7m8@_wjuD_f8 zYmwWkpiN=h!bL4}Tm0Mo_l{p1y}fMnt1mkG)D1!NX8+$>si*n z7-~wyD;5@jMzPP*nu{Hk#{*-a^{5q{^YX6iBCUS^{L?bQpP&EH)8_mAq&GQl-r}R&*~eEpip|*I&TH{bzN!+ksOnE=2?r+v2egtPW0kxU0I|OPxvjg3R~@@0C*`CQCTja zOsfXrm39zSo}CQLf!09Vpw76 + + #FFFFFF + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/values/ic_launcher_background.xml b/Source/GUI/Android/tvapp/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000000..c5d5899fdf --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/values/strings.xml b/Source/GUI/Android/tvapp/src/main/res/values/strings.xml new file mode 100644 index 0000000000..43e0adbc7f --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/values/strings.xml @@ -0,0 +1,37 @@ + + MediaInfo + Browse + Settings + About + Loading… + No media found in MediaStore + Error + Storage Access Required + To browse your media files, %1$s needs permission to access your local storage. + Grant Permission + Appearance + MediaInfoLib + Dark mode + Currently enabled + Currently disabled + File sorting order + The sorting order of media files in the media browser screen + Date modified + File path + File name + Legacy stream display + Display backward compatibility information for HD/HE audio and 3D/HDR video streams + Display captions + Handling of 608/708 streams + When content or a command is detected + When content is detected + Even when no content or command is detected + MediaInfo v%MI_VERSION% using MediaInfoLib v%MIL_VERSION% + Copyright © 2002-2025 MediaArea.net SARL. + + MediaInfo provides easy access to technical and tag information about video and audio files.\n + Except the Mac App Store graphical user interface, it is open-source software, which means that it is free of charge to the end user and developers have freedom to study, to improve and to redistribute the program (BSD license) + + ReportActivity + Please wait + \ No newline at end of file diff --git a/Source/GUI/Android/tvapp/src/main/res/values/themes.xml b/Source/GUI/Android/tvapp/src/main/res/values/themes.xml new file mode 100644 index 0000000000..1fc6040dc9 --- /dev/null +++ b/Source/GUI/Android/tvapp/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +