Skip to content
Open
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
16 changes: 12 additions & 4 deletions .github/locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
START_MARKER = "/* begin language list */"
END_MARKER = "/* end language list */"
XML_NAME = "app/src/main/res/values-b+"
COMPOSE_XML_NAME = "composeApp/src/commonMain/composeResources/values-"
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
INDENT = " "*4

Expand All @@ -24,21 +25,28 @@
name, iso = lang.groups()
languages[iso] = name

# Add not yet added langs
# Add not yet added langs from app/src/main/res (BCP 47 format: values-b+xx)
for folder in glob.glob(f"{XML_NAME}*"):
iso = folder[len(XML_NAME):].replace("+", "-")
if iso not in languages.keys():
entry = iso_map.get(iso.lower(), {'nativeName':iso}) # fallback to iso code if not found
entry = iso_map.get(iso.lower(), {'nativeName': iso}) # fallback to iso code if not found
languages[iso] = entry['nativeName'].split(',')[0] # first name if there are multiple

# Add not yet added langs from composeResources (simple ISO 639-1 format: values-xx)
for folder in glob.glob(f"{COMPOSE_XML_NAME}*"):
iso = folder[len(COMPOSE_XML_NAME):]
if iso not in languages.keys():
entry = iso_map.get(iso.lower(), {'nativeName': iso})
languages[iso] = entry['nativeName'].split(',')[0]

# Create pairs
pairs = []
for iso in sorted(languages, key=lambda iso: languages[iso].lower()): # sort by language name
name = languages[iso]
pairs.append(f'{INDENT}Pair("{name}", "{iso}"),')

# Update settings file
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
open(SETTINGS_PATH, "w+", encoding='utf-8').write(
before_src +
START_MARKER +
"\n" +
Expand All @@ -48,7 +56,7 @@
after_src
)

# Go through each values.xml file and fix escaped \@string
# Fix escaped \@string in app/src/main/res only (Android-specific pattern)
for file in glob.glob(f"{XML_NAME}*/strings.xml"):
try:
tree = ET.parse(file)
Expand Down
13 changes: 13 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.dokka)
}

val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())

// TODO: Move to composeApp, into composeResources
abstract class GenerateGitHashTask : DefaultTask() {

@get:InputFile
Expand Down Expand Up @@ -111,6 +113,7 @@ android {
// Reads local.properties
val localProperties = gradleLocalProperties(rootDir, project.providers)

// TODO: Move BUILD_DATE to composeApp via buildkonfig
buildConfigField(
"long",
"BUILD_DATE",
Expand Down Expand Up @@ -188,6 +191,7 @@ android {
buildFeatures {
buildConfig = true
viewBinding = true
compose = true
}

packaging {
Expand Down Expand Up @@ -270,6 +274,15 @@ dependencies {
implementation(libs.nicehttp) // HTTP Lib

implementation(project(":library"))

implementation(project(":composeApp"))
implementation(libs.activity.compose)
implementation(libs.coil.compose)
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.resources)
}

tasks.register<Jar>("androidSourcesJar") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.lagradost.cloudstream3.ui.compose.settings

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UiImage
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream4.compose.components.ProfileImage
import com.lagradost.cloudstream4.compose.screens.settings.SettingsCategory
import com.lagradost.cloudstream4.compose.screens.settings.SettingsProfileState
import com.lagradost.cloudstream4.compose.screens.settings.SettingsScreen
import com.lagradost.cloudstream4.compose.screens.settings.SettingsVersionState
import com.lagradost.cloudstream4.compose.theme.CloudStreamTheme
import com.lagradost.cloudstream4.compose.theme.loadPrimaryColor
import com.lagradost.cloudstream4.compose.theme.loadThemeMode
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone

/**
* TODO: This Fragment is a temporary bridge between the old View-based navigation system
* and the new Compose UI. It will be removed as part of the Navigation3 migration, which
* replaces the NavGraph/Fragment back stack with a fully KMP-compatible navigation system.
* At that point, screens will be called directly as composables with no Fragment
* or NavGraph involvement, once:
* 1. All fragments have been migrated to Compose screens in :composeApp
* 2. Navigation3 has been adopted, replacing the NavGraph action ID system
* 3. More shared logic is available in :composeApp
*/
class SettingsFragment : Fragment() {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)

setContent {
val profile = buildProfileState()
val version = buildVersionState()
CloudStreamTheme(
mode = context.loadThemeMode(),
primaryColor = context.loadPrimaryColor(),
) {
SettingsScreen(
profile = profile,
version = version,
onNavigate = ::navigateTo,
onVersionLongClick = {
val v = version.appVersion
val h = version.commitHash
val d = version.buildDate
clipboardHelper(txt(R.string.extension_version), "$v $h $d")
},
)
}
}
}

private fun buildProfileState(): SettingsProfileState {
for (syncApi in AccountManager.allApis) {
val login = syncApi.authUser() ?: continue
if (login.profilePicture.isNullOrEmpty()) continue
return SettingsProfileState(
name = login.name ?: "",
profilePictureUrl = login.profilePicture,
)
}

val account = runCatching {
DataStoreHelper.accounts.firstOrNull {
it.keyIndex == DataStoreHelper.selectedKeyIndex
} ?: DataStoreHelper.getDefaultAccount(requireActivity())
}.getOrNull()

val profileImage = when (account?.defaultImageIndex) {
0 -> ProfileImage.DARK_BLUE
1 -> ProfileImage.BLUE
2 -> ProfileImage.ORANGE
3 -> ProfileImage.PINK
4 -> ProfileImage.PURPLE
5 -> ProfileImage.RED
6 -> ProfileImage.TEAL
else -> ProfileImage.DARK_BLUE
}

return SettingsProfileState(
name = account?.name ?: "",
profilePictureUrl = (account?.image as? UiImage.Image)?.url,
profileImage = profileImage,
)
}

private fun buildVersionState(): SettingsVersionState {
val buildDate = SimpleDateFormat
.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.getDefault())
.apply { timeZone = TimeZone.getTimeZone("UTC") }
.format(Date(BuildConfig.BUILD_DATE))
.replace("UTC", "")

return SettingsVersionState(
appVersion = BuildConfig.VERSION_NAME,
commitHash = activity?.currentCommitHash() ?: "",
buildDate = buildDate,
)
}

private fun navigateTo(category: SettingsCategory) {
val actionId = when (category) {
SettingsCategory.GENERAL -> R.id.action_navigation_global_to_navigation_settings_general
SettingsCategory.PLAYER -> R.id.action_navigation_global_to_navigation_settings_player
SettingsCategory.PROVIDERS -> R.id.action_navigation_global_to_navigation_settings_providers
SettingsCategory.LAYOUT -> R.id.action_navigation_global_to_navigation_settings_ui
SettingsCategory.UPDATES -> R.id.action_navigation_global_to_navigation_settings_updates
SettingsCategory.ACCOUNTS -> R.id.action_navigation_global_to_navigation_settings_account
SettingsCategory.EXTENSIONS -> R.id.action_navigation_global_to_navigation_settings_extensions
}
activity?.navigate(actionId, Bundle())
}
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,20 @@
package com.lagradost.cloudstream3.ui.settings

import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream4.utils.DeviceLayout

object Globals {
var beneneCount = 0

const val PHONE : Int = 0b001
const val TV : Int = 0b010
const val EMULATOR : Int = 0b100
private const val INVALID = -1
private var layoutId = INVALID

private fun Context.getLayoutInt(): Int {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getInt(this.getString(R.string.app_layout_key), -1)
}

private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV
val model = Build.MODEL.lowercase()
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
"AFT"
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
}

private fun Context.layoutIntCorrected(): Int {
return when(getLayoutInt()) {
-1 -> if (isAutoTv()) TV else PHONE
0 -> PHONE
1 -> TV
2 -> EMULATOR
else -> PHONE
}
}
const val PHONE: Int = DeviceLayout.PHONE
const val TV: Int = DeviceLayout.TV
const val EMULATOR: Int = DeviceLayout.EMULATOR

fun Context.updateTv() {
layoutId = layoutIntCorrected()
DeviceLayout.update()
}

/** Returns true if the current orientation is landscape. */
fun isLandscape(): Boolean =
isLayout(TV or EMULATOR) ||
Resources.getSystem().configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
fun isLandscape(): Boolean = DeviceLayout.isLandscape()

/** Returns true if the layout is any of the flags,
* so isLayout(TV or EMULATOR) is a valid statement for checking if the layout is in the emulator
* or tv. Auto will become the "TV" or the "PHONE" layout.
*
* Valid flags are: PHONE, TV, EMULATOR
* */
fun isLayout(flags: Int) : Boolean {
return (layoutId and flags) != 0
}
fun isLayout(flags: Int): Boolean = DeviceLayout.isLayout(flags)
}
Loading