Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ plugins {

android {
namespace = "com.arflix.tv"
compileSdk = 35
compileSdk = 36

flavorDimensions += "distribution"

Expand Down Expand Up @@ -54,10 +54,12 @@ android {
create("play") {
dimension = "distribution"
buildConfigField("Boolean", "SELF_UPDATE_ENABLED", "false")
buildConfigField("Boolean", "FEATURE_PLUGINS_ENABLED", "false")
}
create("sideload") {
dimension = "distribution"
buildConfigField("Boolean", "SELF_UPDATE_ENABLED", "true")
buildConfigField("Boolean", "FEATURE_PLUGINS_ENABLED", "true")
}
}

Expand Down Expand Up @@ -151,6 +153,7 @@ android {

packaging {
resources {
excludes += "/META-INF/versions/9/OSGI-INF/MANIFEST.MF"
excludes += setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/LICENSE*",
Expand Down Expand Up @@ -439,3 +442,31 @@ detekt {
}


kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
freeCompilerArgs.add("-Xskip-metadata-version-check")
}
}

dependencies {
ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")
annotationProcessor("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")

// Plugin system dependencies (Sideload flavor only)
add("sideloadImplementation", files("libs/quickjs-kt-android-1.0.5-nuvio.aar"))
add("sideloadImplementation", "com.fasterxml.jackson.core:jackson-databind:2.17.0")
add("sideloadImplementation", "com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
add("sideloadImplementation", "com.github.Blatzar:NiceHttp:0.4.11")
add("sideloadImplementation", "org.conscrypt:conscrypt-android:2.5.3")
add("sideloadImplementation", "com.github.recloudstream.cloudstream:library-android:v4.7.0") {
exclude(group = "org.mozilla", module = "rhino")
}
add("sideloadImplementation", "org.webjars.npm:crypto-js:4.2.0")

// Runtime helpers used by the sideload plugin extractor stack.
add("sideloadImplementation", "org.mozilla:rhino:1.8.1")
add("sideloadImplementation", "com.google.re2j:re2j:1.8")
}


43 changes: 28 additions & 15 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@
# Log stripping for release builds
# Remove ALL logs for maximum performance
# ============================================
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
public static int i(...);
public static int w(...);
public static int e(...);
public static int wtf(...);
}
# -assumenosideeffects class android.util.Log {
# public static int v(...);
# public static int d(...);
# public static int i(...);
# public static int w(...);
# public static int e(...);
# public static int wtf(...);
# }

# Also strip ALL our custom AppLogger methods
-assumenosideeffects class com.arflix.tv.util.AppLogger {
public static void v(...);
public static void d(...);
public static void i(...);
public static void w(...);
public static void e(...);
}
# -assumenosideeffects class com.arflix.tv.util.AppLogger {
# public static void v(...);
# public static void d(...);
# public static void i(...);
# public static void w(...);
# public static void e(...);
# }

# Strip Kotlin debug assertions in release
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
Expand Down Expand Up @@ -120,6 +120,19 @@
-dontwarn javax.script.**
-dontwarn org.mozilla.javascript.**

# CloudStream API & Dependencies (External plugins are compiled against these)
-keep class com.lagradost.** { *; }
-keep interface com.lagradost.** { *; }
-dontwarn com.lagradost.**

-keep class org.jsoup.** { *; }
-keep interface org.jsoup.** { *; }
-dontwarn org.jsoup.**

-keep class com.fasterxml.jackson.** { *; }
-keep interface com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.**

# ============================================
# Hilt / Dagger - KEEP EVERYTHING
# ============================================
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/kotlin/com/arflix/tv/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ class MainActivity : ComponentActivity() {
}
val isRtl = remember(appLanguage) {
val lang = java.util.Locale.forLanguageTag(appLanguage.replace('_', '-')).language
lang in listOf("ar", "he", "fa", "ur")
lang in listOf("ar", "he", "iw", "fa", "ur")
}
CompositionLocalProvider(
androidx.compose.ui.platform.LocalContext provides localizedContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ class MediaRepository @Inject constructor(
/** TMDB content language (e.g. "en-US", "fr-FR", "nl-NL"). Null = TMDB default (English). */
@Volatile
var contentLanguage: String? = null
set(value) {
field = value?.replace("iw", "he")?.replace('_', '-')
}

// === IN-MEMORY CACHE FOR PERFORMANCE ===
private data class CacheEntry<T>(val data: T, val timestamp: Long)
Expand Down
24 changes: 22 additions & 2 deletions app/src/main/kotlin/com/arflix/tv/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,30 @@ object AppModule {

@Provides
@Singleton
fun provideTmdbApi(okHttpClient: OkHttpClient): TmdbApi {
fun provideTmdbApi(okHttpClient: OkHttpClient, @dagger.hilt.android.qualifiers.ApplicationContext context: android.content.Context): TmdbApi {
val tmdbClient = okHttpClient.newBuilder()
.addInterceptor { chain ->
val original = chain.request()
val originalHttpUrl = original.url

val langPrefs = context.getSharedPreferences("app_locale", android.content.Context.MODE_PRIVATE)
val lang = langPrefs.getString("locale_tag", "en-US") ?: "en-US"

// Only inject if it's not the default English. Map "iw" to "he".
val urlBuilder = originalHttpUrl.newBuilder()
if (lang != "en-US") {
val tmdbLang = lang.replace("iw", "he").replace('_', '-')
urlBuilder.setQueryParameter("language", tmdbLang)
}

val requestBuilder = original.newBuilder().url(urlBuilder.build())
chain.proceed(requestBuilder.build())
}
.build()

return Retrofit.Builder()
.baseUrl(Constants.TMDB_BASE_URL)
.client(okHttpClient)
.client(tmdbClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TmdbApi::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,13 @@ fun ContinueWatchingCard(

Text(
text = item.title,
style = ArvioSkin.typography.cardTitle,
style = ArvioSkin.typography.cardTitle.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 6f
)
),
color = if (isFocused) ArvioSkin.colors.textPrimary else ArvioSkin.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
Expand All @@ -196,7 +202,13 @@ fun ContinueWatchingCard(
if (meta != null) {
Text(
text = meta,
style = ArvioSkin.typography.caption,
style = ArvioSkin.typography.caption.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 4f
)
),
color = ArvioSkin.colors.textMuted.copy(alpha = 0.85f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
Expand Down Expand Up @@ -274,15 +286,27 @@ fun ContinueWatchingCardCompact(
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.title,
style = ArvioSkin.typography.cardTitle,
style = ArvioSkin.typography.cardTitle.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 6f
)
),
color = ArvioSkin.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (episodeInfo != null) {
Text(
text = episodeInfo,
style = ArvioSkin.typography.caption,
style = ArvioSkin.typography.caption.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 4f
)
),
color = ArvioSkin.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
Expand Down
40 changes: 35 additions & 5 deletions app/src/main/kotlin/com/arflix/tv/ui/components/MediaCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,20 @@ fun MediaCard(

Text(
text = item.title,
style = if (isMobile) ArvioSkin.typography.cardTitle.copy(fontSize = 14.sp)
else ArvioSkin.typography.cardTitle,
style = if (isMobile) ArvioSkin.typography.cardTitle.copy(
fontSize = 14.sp,
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 6f
)
) else ArvioSkin.typography.cardTitle.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 6f
)
),
color = if (visualFocused) {
ArvioSkin.colors.textPrimary
} else {
Expand All @@ -475,7 +487,13 @@ fun MediaCard(
}
Text(
text = subtitle,
style = ArvioSkin.typography.caption,
style = ArvioSkin.typography.caption.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 4f
)
),
color = ArvioSkin.colors.textMuted.copy(alpha = 0.85f),
maxLines = subtitleMaxLines,
overflow = TextOverflow.Ellipsis,
Expand Down Expand Up @@ -621,7 +639,13 @@ fun PosterCard(

Text(
text = item.title,
style = ArvioSkin.typography.caption,
style = ArvioSkin.typography.caption.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 4f
)
),
color = ArvioSkin.colors.textPrimary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
Expand All @@ -630,7 +654,13 @@ fun PosterCard(
if (item.year.isNotBlank()) {
Text(
text = item.year,
style = ArvioSkin.typography.caption,
style = ArvioSkin.typography.caption.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = androidx.compose.ui.graphics.Color.Black,
offset = androidx.compose.ui.geometry.Offset(2f, 2f),
blurRadius = 4f
)
),
color = ArvioSkin.colors.textMuted.copy(alpha = 0.65f),
)
}
Expand Down
17 changes: 13 additions & 4 deletions app/src/main/kotlin/com/arflix/tv/ui/components/StreamSelector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ fun StreamSelector(
hasStreamingAddons = hasStreamingAddons,
completedAddons = completedAddons,
totalAddons = totalAddons,
elapsedSeconds = elapsedSeconds,
pluginScrapersLoading = pluginScrapersLoading,
loadingPluginNames = loadingPluginNames,
onFilterSelected = { index ->
Expand Down Expand Up @@ -529,6 +530,7 @@ fun StreamSelector(
Spacer(modifier = Modifier.height(12.dp))
Text(
text = buildString {
if (elapsedSeconds > 0) append("${elapsedSeconds}s \u2022 ")
if (loadingPluginNames.isNotEmpty()) append(stringResource(R.string.plugins_loading, loadingPluginNames.joinToString(", ")))
else if (pluginScrapersLoading) append(stringResource(R.string.plugins_loading, "..."))
else if (totalAddons > 0) append("Searching addons ($completedAddons/$totalAddons)...")
Expand Down Expand Up @@ -627,6 +629,7 @@ private fun OledSourceSelectorTv(
hasStreamingAddons: Boolean,
completedAddons: Int,
totalAddons: Int,
elapsedSeconds: Int = 0,
pluginScrapersLoading: Boolean,
loadingPluginNames: Set<String>,
onFilterSelected: (Int) -> Unit,
Expand Down Expand Up @@ -671,7 +674,8 @@ private fun OledSourceSelectorTv(
sourceCount = streams.size,
completedAddons = completedAddons,
totalAddons = totalAddons,
isLoading = isLoading
isLoading = isLoading,
elapsedSeconds = elapsedSeconds
),
style = ArflixTypography.caption.copy(fontSize = 13.sp),
color = OledMutedText,
Expand Down Expand Up @@ -737,6 +741,7 @@ private fun OledSourceSelectorTv(
completedAddons = completedAddons,
totalAddons = totalAddons,
hasStreamingAddons = hasStreamingAddons,
elapsedSeconds = elapsedSeconds,
pluginScrapersLoading = pluginScrapersLoading,
loadingPluginNames = loadingPluginNames
)
Expand Down Expand Up @@ -957,13 +962,15 @@ private fun sourceStatusText(
sourceCount: Int,
completedAddons: Int,
totalAddons: Int,
isLoading: Boolean
isLoading: Boolean,
elapsedSeconds: Int = 0
): String {
val remaining = (totalAddons - completedAddons).coerceAtLeast(0)
val elapsed = if (elapsedSeconds > 0 && isLoading) "${elapsedSeconds}s \u2022 " else ""
return when {
isLoading && totalAddons > 0 && remaining > 0 ->
"$sourceCount found - still checking $remaining ${if (remaining == 1) "addon" else "addons"}"
isLoading -> "$sourceCount found - searching sources"
"${elapsed}$sourceCount found - still checking $remaining ${if (remaining == 1) "addon" else "addons"}"
isLoading -> "${elapsed}$sourceCount found - searching sources"
totalAddons > 0 -> "$sourceCount found - $completedAddons/$totalAddons addons checked"
else -> "$sourceCount found"
}
Expand Down Expand Up @@ -1539,6 +1546,7 @@ private fun SourceEmptyState(
completedAddons: Int,
totalAddons: Int,
hasStreamingAddons: Boolean,
elapsedSeconds: Int = 0,
pluginScrapersLoading: Boolean = false,
loadingPluginNames: Set<String> = emptySet(),
message: String? = null
Expand All @@ -1560,6 +1568,7 @@ private fun SourceEmptyState(
Spacer(modifier = Modifier.height(14.dp))
Text(
text = buildString {
if (elapsedSeconds > 0) append("${elapsedSeconds}s \u2022 ")
if (loadingPluginNames.isNotEmpty()) append(stringResource(R.string.plugins_loading, loadingPluginNames.joinToString(", ")))
else if (pluginScrapersLoading) append(stringResource(R.string.plugins_loading, "..."))
else if (totalAddons > 0) append("Searching addons ($completedAddons/$totalAddons)...")
Expand Down
Loading
Loading