diff --git a/.github/locales.py b/.github/locales.py index 6127d9d806e..e9b8fdbc844 100644 --- a/.github/locales.py +++ b/.github/locales.py @@ -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 @@ -24,13 +25,20 @@ 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 @@ -38,7 +46,7 @@ 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" + @@ -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) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1b1aefab258..3889266b081 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 @@ -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", @@ -188,6 +191,7 @@ android { buildFeatures { buildConfig = true viewBinding = true + compose = true } packaging { @@ -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("androidSourcesJar") { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/compose/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/compose/settings/SettingsFragment.kt new file mode 100644 index 00000000000..5b1f1d1825a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/compose/settings/SettingsFragment.kt @@ -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()) + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt index 93e469a4d64..84bc29ee703 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt @@ -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) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index e41109b5982..8db49cf77b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -1,7 +1,5 @@ package com.lagradost.cloudstream3.ui.settings -import android.os.Bundle -import android.util.Log import android.view.View import android.widget.ImageView import androidx.annotation.StringRes @@ -12,39 +10,21 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar -import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe -import com.lagradost.cloudstream3.syncproviders.AccountManager -import com.lagradost.cloudstream3.syncproviders.AuthRepo -import com.lagradost.cloudstream3.ui.BaseFragment -import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.errorProfilePic import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLandscape import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash -import com.lagradost.cloudstream3.utils.ImageLoader.loadImage -import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding -import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.toPx -import com.lagradost.cloudstream3.utils.getImageFromDrawable -import com.lagradost.cloudstream3.utils.txt import java.io.File -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone -class SettingsFragment : BaseFragment( - BaseFragment.BindingCreator.Inflate(MainSettingsBinding::inflate) -) { +// Moved to com.lagradost.cloudstream3.ui.compose.settings.SettingsFragment +// TODO: Move companion methods to helpers and remove this class entirely +class SettingsFragment { companion object { fun PreferenceFragmentCompat?.getPref(id: Int): Preference? { if (this == null) return null @@ -168,98 +148,4 @@ class SettingsFragment : BaseFragment( return size } } - - override fun fixLayout(view: View) { - fixSystemBarsPadding( - view, - padBottom = isLandscape(), - padLeft = isLayout(TV or EMULATOR) - ) - } - - override fun onBindingCreated(binding: MainSettingsBinding) { - fun navigate(id: Int) { - activity?.navigate(id, Bundle()) - } - - /** used to debug leaks - showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : - ${VideoDownloadManager.downloadProgressEvent.size}") **/ - - fun hasProfilePictureFromAccountManagers(accountManagers: Array): Boolean { - for (syncApi in accountManagers) { - val login = syncApi.authUser() - val pic = login?.profilePicture ?: continue - - binding.settingsProfilePic.let { imageView -> - imageView.loadImage(pic) { - // Fallback to random error drawable - error { getImageFromDrawable(context ?: return@error null, errorProfilePic) } - } - } - binding.settingsProfileText.text = login.name - return true // sync profile exists - } - return false // not syncing - } - - // display local account information if not syncing - if (!hasProfilePictureFromAccountManagers(AccountManager.allApis)) { - val activity = activity ?: return - val currentAccount = try { - DataStoreHelper.accounts.firstOrNull { - it.keyIndex == DataStoreHelper.selectedKeyIndex - } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } - - } catch (t: IllegalStateException) { - Log.e("AccountManager", "Activity not found", t) - null - } - - binding.settingsProfilePic.loadImage(currentAccount?.image) - binding.settingsProfileText.text = currentAccount?.name - } - - binding.apply { - listOf( - settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, - settingsPlayer to R.id.action_navigation_global_to_navigation_settings_player, - settingsCredits to R.id.action_navigation_global_to_navigation_settings_account, - settingsUi to R.id.action_navigation_global_to_navigation_settings_ui, - settingsProviders to R.id.action_navigation_global_to_navigation_settings_providers, - settingsUpdates to R.id.action_navigation_global_to_navigation_settings_updates, - settingsExtensions to R.id.action_navigation_global_to_navigation_settings_extensions, - ).forEach { (view, navigationId) -> - view.apply { - setOnClickListener { - navigate(navigationId) - } - if (isLayout(TV)) { - isFocusable = true - isFocusableInTouchMode = true - } - } - } - - // Default focus on TV - if (isLayout(TV)) { - settingsGeneral.requestFocus() - } - } - - val appVersion = BuildConfig.VERSION_NAME - val commitHash = activity?.currentCommitHash() ?: "" - val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, - Locale.getDefault() - ).apply { timeZone = TimeZone.getTimeZone("UTC") - }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") - - binding.appVersion.text = appVersion - binding.buildDate.text = buildTimestamp - binding.commitHash.text = commitHash - binding.appVersionInfo.setOnLongClickListener { - clipboardHelper(txt(R.string.extension_version), "$appVersion $commitHash $buildTimestamp") - true - } - } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml deleted file mode 100644 index 5c05599e828..00000000000 --- a/app/src/main/res/layout/main_settings.xml +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 27f186a0074..1bc7195d0a2 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -356,14 +356,13 @@ + app:popExitAnim="@anim/exit_anim" /> = Build.VERSION_CODES.S) + Color(resources.getColor(android.R.color.system_accent1_200, null)) + else CloudStreamPrimaryColor.NORMAL.color +} + +@Composable +actual fun resolveDynamicSecondaryColor(): Color { + val resources = LocalContext.current.resources + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + Color(resources.getColor(android.R.color.system_accent2_200, null)) + else CloudStreamPrimaryColor.NORMAL.color +} diff --git a/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.android.kt b/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.android.kt new file mode 100644 index 00000000000..c6f3319bfff --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.android.kt @@ -0,0 +1,51 @@ +package com.lagradost.cloudstream4.compose.theme + +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +@Composable +actual fun resolveDynamicTheme(): CloudStreamColorScheme { + val context = LocalContext.current + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + context.buildMonetScheme() + else darkScheme() +} + +@RequiresApi(Build.VERSION_CODES.S) +fun Context.buildMonetScheme(): CloudStreamColorScheme { + val isSystemDark = (resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + + return if (isSystemDark) { + CloudStreamColorScheme( + background = Color(getColor(android.R.color.system_neutral1_900)), + surfaceVariant = Color(getColor(android.R.color.system_neutral1_800)), + surface = Color(getColor(android.R.color.system_neutral1_800)), + surfaceContainer = Color(getColor(android.R.color.system_neutral1_800)), + onBackground = Color(getColor(android.R.color.system_neutral1_100)), + onSurfaceVariant = Color(getColor(android.R.color.system_neutral2_400)), + icon = Color(getColor(android.R.color.system_neutral1_100)), + primary = Color(getColor(android.R.color.system_accent1_200)), + ongoing = CloudStreamPalette.Ongoing, + isLight = false, + ) + } else { + CloudStreamColorScheme( + background = Color(getColor(android.R.color.system_neutral1_10)), + surfaceVariant = Color(getColor(android.R.color.system_neutral1_100)), + surface = Color(getColor(android.R.color.system_neutral1_100)), + surfaceContainer = Color(getColor(android.R.color.system_neutral1_100)), + onBackground = Color(getColor(android.R.color.system_neutral1_900)), + onSurfaceVariant = Color(getColor(android.R.color.system_neutral2_600)), + icon = Color(getColor(android.R.color.system_neutral1_900)), + primary = Color(getColor(android.R.color.system_accent1_600)), + ongoing = CloudStreamPalette.Ongoing, + isLight = true, + ) + } +} diff --git a/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/compose/theme/ThemePreferenceHelper.android.kt b/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/compose/theme/ThemePreferenceHelper.android.kt new file mode 100644 index 00000000000..a64b6a31454 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/compose/theme/ThemePreferenceHelper.android.kt @@ -0,0 +1,58 @@ +package com.lagradost.cloudstream4.compose.theme + +import android.content.Context +import android.os.Build +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream4.preferences.PreferenceDefaults +import com.lagradost.cloudstream4.preferences.PreferenceKeys + +fun Context.loadThemeMode(): CloudStreamThemeMode { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + return when (prefs.getString(PreferenceKeys.APP_THEME, PreferenceDefaults.APP_THEME)) { + "System" -> CloudStreamThemeMode.FollowSystem + "Black" -> CloudStreamThemeMode.Dark + "Light" -> CloudStreamThemeMode.Light + "Amoled" -> CloudStreamThemeMode.Amoled + "AmoledLight" -> CloudStreamThemeMode.Amoled + "Dracula" -> CloudStreamThemeMode.Dracula + "Lavender" -> CloudStreamThemeMode.Lavender + "SilentBlue" -> CloudStreamThemeMode.SilentBlue + "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + CloudStreamThemeMode.Dynamic + } else CloudStreamThemeMode.Dark + else -> CloudStreamThemeMode.Dark + } +} + +fun Context.loadPrimaryColor(): CloudStreamPrimaryColor { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + return when (prefs.getString(PreferenceKeys.PRIMARY_COLOR, PreferenceDefaults.PRIMARY_COLOR)) { + "Normal" -> CloudStreamPrimaryColor.NORMAL + "Blue" -> CloudStreamPrimaryColor.BLUE + "Purple" -> CloudStreamPrimaryColor.PURPLE + "Green" -> CloudStreamPrimaryColor.GREEN + "GreenApple" -> CloudStreamPrimaryColor.GREEN_APPLE + "Red" -> CloudStreamPrimaryColor.RED + "Banana" -> CloudStreamPrimaryColor.BANANA + "Party" -> CloudStreamPrimaryColor.PARTY + "Pink" -> CloudStreamPrimaryColor.PINK + "CarnationPink" -> CloudStreamPrimaryColor.CARNATION_PINK + "Maroon" -> CloudStreamPrimaryColor.MAROON + "DarkGreen" -> CloudStreamPrimaryColor.DARK_GREEN + "NavyBlue" -> CloudStreamPrimaryColor.NAVY_BLUE + "Grey" -> CloudStreamPrimaryColor.GREY + "White" -> CloudStreamPrimaryColor.WHITE + "Brown" -> CloudStreamPrimaryColor.BROWN + "Orange" -> CloudStreamPrimaryColor.ORANGE + "DandelionYellow" -> CloudStreamPrimaryColor.DANDELION_YELLOW + "CoolBlue" -> CloudStreamPrimaryColor.COOL_BLUE + "Lavender" -> CloudStreamPrimaryColor.LAVENDER + "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + CloudStreamPrimaryColor.DYNAMIC + } else CloudStreamPrimaryColor.NORMAL + "Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + CloudStreamPrimaryColor.DYNAMIC_TWO + } else CloudStreamPrimaryColor.NORMAL + else -> CloudStreamPrimaryColor.NORMAL + } +} diff --git a/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.android.kt b/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.android.kt new file mode 100644 index 00000000000..345bc645ebe --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.android.kt @@ -0,0 +1,38 @@ +package com.lagradost.cloudstream4.utils + +import android.app.UiModeManager +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import androidx.preference.PreferenceManager +import com.lagradost.api.getContext +import com.lagradost.cloudstream4.preferences.PreferenceDefaults +import com.lagradost.cloudstream4.preferences.PreferenceKeys + +internal actual object DeviceInfo { + actual fun getDeviceType(): DeviceType { + val context = getContext() as? Context ?: return DeviceType.PHONE + val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? + val isTelevisionMode = uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION + val model = Build.MODEL.lowercase() + return when { + isTelevisionMode + || Build.MODEL.contains("AFT") // AFT = Fire TV + || model.contains("firestick") + || model.contains("fire tv") + || model.contains("chromecast") -> DeviceType.TV + else -> DeviceType.PHONE + } + } + + actual fun isLandscape(): Boolean { + val context = getContext() as? Context ?: return false + return context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + } + + actual fun getLayoutPreference(): Int { + val context = getContext() as? Context ?: return PreferenceDefaults.APP_LAYOUT + return PreferenceManager.getDefaultSharedPreferences(context) + .getInt(PreferenceKeys.APP_LAYOUT, PreferenceDefaults.APP_LAYOUT) + } +} diff --git a/composeApp/src/commonMain/composeResources/drawable/profile_bg_blue.jpg b/composeApp/src/commonMain/composeResources/drawable/profile_bg_blue.jpg new file mode 100644 index 00000000000..e573439b04e Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/profile_bg_blue.jpg differ diff --git a/composeApp/src/commonMain/composeResources/drawable/profile_bg_dark_blue.jpg b/composeApp/src/commonMain/composeResources/drawable/profile_bg_dark_blue.jpg new file mode 100644 index 00000000000..d59e4888c64 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/profile_bg_dark_blue.jpg differ diff --git a/composeApp/src/commonMain/composeResources/drawable/profile_bg_orange.jpg b/composeApp/src/commonMain/composeResources/drawable/profile_bg_orange.jpg new file mode 100644 index 00000000000..a97e7179f99 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/profile_bg_orange.jpg differ diff --git a/composeApp/src/commonMain/composeResources/drawable/profile_bg_pink.jpg b/composeApp/src/commonMain/composeResources/drawable/profile_bg_pink.jpg new file mode 100644 index 00000000000..9d4940f0d9f Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/profile_bg_pink.jpg differ diff --git a/composeApp/src/commonMain/composeResources/drawable/profile_bg_purple.jpg b/composeApp/src/commonMain/composeResources/drawable/profile_bg_purple.jpg new file mode 100644 index 00000000000..15723dba35e Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/profile_bg_purple.jpg differ diff --git a/composeApp/src/commonMain/composeResources/drawable/profile_bg_red.jpg b/composeApp/src/commonMain/composeResources/drawable/profile_bg_red.jpg new file mode 100644 index 00000000000..6a27ff31315 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/profile_bg_red.jpg differ diff --git a/composeApp/src/commonMain/composeResources/drawable/profile_bg_teal.jpg b/composeApp/src/commonMain/composeResources/drawable/profile_bg_teal.jpg new file mode 100644 index 00000000000..9323665088f Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/profile_bg_teal.jpg differ diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 00000000000..cd787cb1e77 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,17 @@ + + Accounts and Security + Sync services & profiles + Extensions + Repositories & plugins + General + Language, downloads & network + Layout + Theme, colors & layout + Player + Playback, subtitles & gestures + Providers + Content language & media type + Updates and Backup + Updates, backup & restore + Profile picture + diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/CloudStreamRipple.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/CloudStreamRipple.kt new file mode 100644 index 00000000000..0f0e5595104 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/CloudStreamRipple.kt @@ -0,0 +1,22 @@ +package com.lagradost.cloudstream4.compose.components + +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ripple +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import com.lagradost.cloudstream4.compose.theme.CloudStreamTheme + +/** + * Applies a ripple indication styled to match the app's selected theme. + */ +fun Modifier.cloudStreamRipple( + interactionSource: MutableInteractionSource, + bounded: Boolean = true, +): Modifier = composed { + val colors = CloudStreamTheme.colors + this.indication( + interactionSource = interactionSource, + indication = ripple(bounded = bounded, color = colors.onBackground), + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/ProfilePicture.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/ProfilePicture.kt new file mode 100644 index 00000000000..9fffc57c700 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/ProfilePicture.kt @@ -0,0 +1,84 @@ +package com.lagradost.cloudstream4.compose.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.lagradost.cloudstream4.compose.theme.CloudStreamTheme +import com.lagradost.cloudstream4.generated.resources.Res +import com.lagradost.cloudstream4.generated.resources.profile_bg_blue +import com.lagradost.cloudstream4.generated.resources.profile_bg_dark_blue +import com.lagradost.cloudstream4.generated.resources.profile_bg_orange +import com.lagradost.cloudstream4.generated.resources.profile_bg_pink +import com.lagradost.cloudstream4.generated.resources.profile_bg_purple +import com.lagradost.cloudstream4.generated.resources.profile_bg_red +import com.lagradost.cloudstream4.generated.resources.profile_bg_teal +import com.lagradost.cloudstream4.generated.resources.profile_picture_desc +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +enum class ProfileImage { + DARK_BLUE, BLUE, ORANGE, PINK, PURPLE, RED, TEAL; +} + +@Composable +private fun ProfileImage.toRes(): DrawableResource = when (this) { + ProfileImage.DARK_BLUE -> Res.drawable.profile_bg_dark_blue + ProfileImage.BLUE -> Res.drawable.profile_bg_blue + ProfileImage.ORANGE -> Res.drawable.profile_bg_orange + ProfileImage.PINK -> Res.drawable.profile_bg_pink + ProfileImage.PURPLE -> Res.drawable.profile_bg_purple + ProfileImage.RED -> Res.drawable.profile_bg_red + ProfileImage.TEAL -> Res.drawable.profile_bg_teal +} + +/** + * Circular profile picture component. + * + * Shows [AsyncImage] from [profilePictureUrl] if not null, + * otherwise falls back to the local [profileImage] background. + * + * @param profileImage Local background image fallback + * @param size Diameter of the circle, default 50.dp + * @param profilePictureUrl Optional remote URL to load via Coil + */ +@Composable +fun ProfilePicture( + profileImage: ProfileImage, + size: Dp = 50.dp, + profilePictureUrl: String? = null, +) { + val colors = CloudStreamTheme.colors + Box( + modifier = Modifier + .size(size) + .border(2.dp, colors.onBackground.copy(alpha = 0.2f), CircleShape) + .clip(CircleShape), + ) { + if (profilePictureUrl != null) { + AsyncImage( + model = profilePictureUrl, + contentDescription = stringResource(Res.string.profile_picture_desc), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } else { + Image( + painter = painterResource(profileImage.toRes()), + contentDescription = stringResource(Res.string.profile_picture_desc), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/TvFocusable.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/TvFocusable.kt new file mode 100644 index 00000000000..3123d59a230 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/TvFocusable.kt @@ -0,0 +1,78 @@ +package com.lagradost.cloudstream4.compose.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import com.lagradost.cloudstream4.compose.theme.CloudStreamTheme + +/** + * Reusable TV focusable modifier with built-in focus border. + * + * @param isTV Whether to use TV focus behavior + * @param onClick Action to perform when item is clicked/selected + * @param focusRequester Optional external FocusRequester + * @param onFocusChanged Optional callback when focus state changes + * @param shape Shape of the focus border + * @param interactionSource MutableInteractionSource for ripple indication + */ +@Composable +fun Modifier.tvFocusable( + isTV: Boolean, + onClick: () -> Unit, + focusRequester: FocusRequester? = null, + onFocusChanged: ((Boolean) -> Unit)? = null, + shape: RoundedCornerShape = RoundedCornerShape(4.dp), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +): Modifier { + val colors = CloudStreamTheme.colors + var isFocused by remember { mutableStateOf(false) } + val focusRequesterLocal = remember { FocusRequester() } + val effectiveFocusRequester = focusRequester ?: focusRequesterLocal + + return if (isTV) { + this + .focusRequester(effectiveFocusRequester) + .onFocusChanged { + isFocused = it.isFocused + onFocusChanged?.invoke(it.isFocused) + } + .focusable() + .border( + width = if (isFocused) 2.dp else 0.dp, + color = if (isFocused) colors.onBackground else Color.Transparent, + shape = shape, + ) + .pointerInput(isFocused) { + detectTapGestures( + onPress = { + val press = PressInteraction.Press(it) + interactionSource.emit(press) + tryAwaitRelease() + interactionSource.emit(PressInteraction.Release(press)) + }, + onTap = { + if (!isFocused) effectiveFocusRequester.requestFocus() + else onClick() + }, + ) + } + } else { + this.clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/settings/SettingsItem.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/settings/SettingsItem.kt new file mode 100644 index 00000000000..a054a522294 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/components/settings/SettingsItem.kt @@ -0,0 +1,67 @@ +package com.lagradost.cloudstream4.compose.components.settings + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.lagradost.cloudstream4.compose.components.cloudStreamRipple +import com.lagradost.cloudstream4.compose.components.tvFocusable +import com.lagradost.cloudstream4.compose.theme.CloudStreamTheme +import com.lagradost.cloudstream4.utils.DeviceLayout + +@Composable +fun SettingsItem( + title: String, + modifier: Modifier = Modifier, + subtitle: String? = null, + icon: ImageVector? = null, + focusRequester: FocusRequester? = null, + onClick: () -> Unit, +) { + val colors = CloudStreamTheme.colors + val isTV = remember { DeviceLayout.isLayout(DeviceLayout.TV) } + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = modifier + .fillMaxWidth() + .tvFocusable( + isTV = isTV, + onClick = onClick, + focusRequester = focusRequester, + interactionSource = interactionSource, + ) + .cloudStreamRipple(interactionSource) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = colors.onBackground, + modifier = Modifier.size(28.dp), + ) + Spacer(modifier = Modifier.width(24.dp)) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + color = colors.onBackground, + style = MaterialTheme.typography.bodyLarge, + ) + if (subtitle != null) { + Text( + text = subtitle, + color = colors.onBackground.copy(alpha = 0.6f), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/AccountCircle.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/AccountCircle.kt new file mode 100644 index 00000000000..a3dbf7febd4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/AccountCircle.kt @@ -0,0 +1,110 @@ +package com.lagradost.cloudstream4.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("CheckReturnValue") +public val account_circle: ImageVector + get() { + if (_account_circle != null) { + return _account_circle!! + } + _account_circle = + ImageVector.Builder( + name = "account_circle", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = PathFillType.Companion.NonZero, + ) { + moveTo(5.85f, 17.1f) + quadTo(7.13f, 16.13f, 8.7f, 15.56f) + reflectiveQuadTo(12f, 15f) + reflectiveQuadToRelative(3.3f, 0.56f) + reflectiveQuadToRelative(2.85f, 1.54f) + quadToRelative(0.88f, -1.03f, 1.36f, -2.33f) + reflectiveQuadTo(20f, 12f) + quadTo(20f, 8.67f, 17.66f, 6.34f) + reflectiveQuadTo(12f, 4f) + quadTo(8.68f, 4f, 6.34f, 6.34f) + reflectiveQuadTo(4f, 12f) + quadToRelative(0f, 1.47f, 0.49f, 2.78f) + quadToRelative(0.49f, 1.3f, 1.36f, 2.33f) + close() + moveTo(9.51f, 11.99f) + quadTo(8.5f, 10.98f, 8.5f, 9.5f) + quadTo(8.5f, 8.02f, 9.51f, 7.01f) + reflectiveQuadTo(12f, 6f) + reflectiveQuadToRelative(2.49f, 1.01f) + reflectiveQuadTo(15.5f, 9.5f) + reflectiveQuadToRelative(-1.01f, 2.49f) + reflectiveQuadTo(12f, 13f) + quadTo(10.53f, 13f, 9.51f, 11.99f) + close() + moveTo(12f, 22f) + quadTo(9.93f, 22f, 8.1f, 21.21f) + quadTo(6.28f, 20.43f, 4.93f, 19.08f) + quadTo(3.58f, 17.73f, 2.79f, 15.9f) + reflectiveQuadTo(2f, 12f) + quadTo(2f, 9.92f, 2.79f, 8.1f) + quadTo(3.58f, 6.27f, 4.93f, 4.93f) + quadTo(6.28f, 3.57f, 8.1f, 2.79f) + quadTo(9.93f, 2f, 12f, 2f) + reflectiveQuadToRelative(3.9f, 0.79f) + reflectiveQuadToRelative(3.17f, 2.14f) + quadToRelative(1.35f, 1.35f, 2.14f, 3.17f) + quadTo(22f, 9.92f, 22f, 12f) + reflectiveQuadToRelative(-0.79f, 3.9f) + reflectiveQuadToRelative(-2.14f, 3.17f) + quadToRelative(-1.35f, 1.35f, -3.17f, 2.14f) + reflectiveQuadTo(12f, 22f) + close() + moveToRelative(2.5f, -2.39f) + quadToRelative(1.18f, -0.39f, 2.15f, -1.11f) + quadTo(15.68f, 17.77f, 14.5f, 17.39f) + reflectiveQuadTo(12f, 17f) + reflectiveQuadTo(9.5f, 17.39f) + quadTo(8.33f, 17.77f, 7.35f, 18.5f) + quadToRelative(0.98f, 0.73f, 2.15f, 1.11f) + reflectiveQuadTo(12f, 20f) + reflectiveQuadToRelative(2.5f, -0.39f) + close() + moveTo(13.08f, 10.58f) + quadTo(13.5f, 10.15f, 13.5f, 9.5f) + reflectiveQuadTo(13.08f, 8.42f) + reflectiveQuadTo(12f, 8f) + reflectiveQuadTo(10.93f, 8.42f) + reflectiveQuadTo(10.5f, 9.5f) + reflectiveQuadToRelative(0.43f, 1.07f) + reflectiveQuadTo(12f, 11f) + reflectiveQuadToRelative(1.08f, -0.43f) + close() + moveTo(12f, 9.5f) + close() + moveToRelative(0f, 9f) + close() + } + } + .build() + return _account_circle!! + } + +private var _account_circle: ImageVector? = null diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Extension.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Extension.kt new file mode 100644 index 00000000000..34c4e7c0b3f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Extension.kt @@ -0,0 +1,110 @@ +package com.lagradost.cloudstream4.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("CheckReturnValue") +public val extension: ImageVector + get() { + if (_extension != null) { + return _extension!! + } + _extension = + ImageVector.Builder( + name = "extension", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = PathFillType.Companion.NonZero, + ) { + moveTo(8.8f, 21f) + horizontalLineTo(5f) + quadTo(4.18f, 21f, 3.59f, 20.41f) + reflectiveQuadTo(3f, 19f) + verticalLineTo(15.2f) + quadToRelative(1.2f, 0f, 2.1f, -0.76f) + reflectiveQuadTo(6f, 12.5f) + reflectiveQuadTo(5.1f, 10.56f) + reflectiveQuadTo(3f, 9.8f) + verticalLineTo(6f) + quadTo(3f, 5.18f, 3.59f, 4.59f) + reflectiveQuadTo(5f, 4f) + horizontalLineTo(9f) + quadTo(9f, 2.95f, 9.73f, 2.22f) + reflectiveQuadTo(11.5f, 1.5f) + reflectiveQuadToRelative(1.78f, 0.72f) + reflectiveQuadTo(14f, 4f) + horizontalLineToRelative(4f) + quadToRelative(0.82f, 0f, 1.41f, 0.59f) + quadTo(20f, 5.18f, 20f, 6f) + verticalLineToRelative(4f) + quadToRelative(1.05f, 0f, 1.78f, 0.72f) + reflectiveQuadTo(22.5f, 12.5f) + reflectiveQuadToRelative(-0.72f, 1.77f) + reflectiveQuadTo(20f, 15f) + verticalLineToRelative(4f) + quadToRelative(0f, 0.82f, -0.59f, 1.41f) + reflectiveQuadTo(18f, 21f) + horizontalLineTo(14.2f) + quadToRelative(0f, -1.25f, -0.79f, -2.13f) + reflectiveQuadTo(11.5f, 18f) + reflectiveQuadTo(9.59f, 18.88f) + reflectiveQuadTo(8.8f, 21f) + close() + moveTo(5f, 19f) + horizontalLineTo(7.13f) + quadToRelative(0.6f, -1.65f, 1.93f, -2.32f) + reflectiveQuadTo(11.5f, 16f) + reflectiveQuadToRelative(2.45f, 0.68f) + reflectiveQuadTo(15.88f, 19f) + horizontalLineTo(18f) + verticalLineTo(13f) + horizontalLineToRelative(2f) + quadToRelative(0.2f, 0f, 0.35f, -0.15f) + reflectiveQuadTo(20.5f, 12.5f) + reflectiveQuadTo(20.35f, 12.15f) + reflectiveQuadTo(20f, 12f) + horizontalLineTo(18f) + verticalLineTo(6f) + horizontalLineTo(12f) + verticalLineTo(4f) + quadTo(12f, 3.8f, 11.85f, 3.65f) + reflectiveQuadTo(11.5f, 3.5f) + reflectiveQuadTo(11.15f, 3.65f) + reflectiveQuadTo(11f, 4f) + verticalLineTo(6f) + horizontalLineTo(5f) + verticalLineTo(8.2f) + quadTo(6.35f, 8.7f, 7.18f, 9.88f) + reflectiveQuadTo(8f, 12.5f) + quadToRelative(0f, 1.42f, -0.82f, 2.6f) + reflectiveQuadTo(5f, 16.8f) + verticalLineTo(19f) + close() + moveToRelative(6.5f, -6.5f) + close() + } + } + .build() + return _extension!! + } + +private var _extension: ImageVector? = null diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/MobileArrowDown.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/MobileArrowDown.kt new file mode 100644 index 00000000000..311be4b445c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/MobileArrowDown.kt @@ -0,0 +1,85 @@ +package com.lagradost.cloudstream4.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("CheckReturnValue") +public val mobile_arrow_down: ImageVector + get() { + if (_mobile_arrow_down != null) { + return _mobile_arrow_down!! + } + _mobile_arrow_down = + ImageVector.Builder( + name = "mobile_arrow_down", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = PathFillType.Companion.NonZero, + ) { + moveTo(7f, 23f) + quadTo(6.18f, 23f, 5.59f, 22.41f) + reflectiveQuadTo(5f, 21f) + verticalLineTo(3f) + quadTo(5f, 2.17f, 5.59f, 1.59f) + reflectiveQuadTo(7f, 1f) + horizontalLineTo(17f) + quadToRelative(0.82f, 0f, 1.41f, 0.59f) + reflectiveQuadTo(19f, 3f) + verticalLineTo(6.1f) + quadToRelative(0.45f, 0.18f, 0.73f, 0.55f) + reflectiveQuadTo(20f, 7.5f) + verticalLineToRelative(2f) + quadToRelative(0f, 0.47f, -0.27f, 0.85f) + reflectiveQuadTo(19f, 10.9f) + verticalLineTo(21f) + quadToRelative(0f, 0.82f, -0.59f, 1.41f) + reflectiveQuadTo(17f, 23f) + horizontalLineTo(7f) + close() + moveTo(7f, 21f) + horizontalLineTo(17f) + verticalLineTo(3f) + horizontalLineTo(7f) + verticalLineTo(21f) + close() + moveToRelative(0f, 0f) + verticalLineTo(3f) + verticalLineTo(21f) + close() + moveToRelative(5f, -5f) + lineToRelative(4f, -4f) + lineTo(14.6f, 10.6f) + lineTo(13f, 12.15f) + verticalLineTo(8f) + horizontalLineTo(11f) + verticalLineToRelative(4.15f) + lineTo(9.4f, 10.6f) + lineTo(8f, 12f) + lineToRelative(4f, 4f) + close() + } + } + .build() + return _mobile_arrow_down!! + } + +private var _mobile_arrow_down: ImageVector? = null diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Palette.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Palette.kt new file mode 100644 index 00000000000..039839cc35f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Palette.kt @@ -0,0 +1,126 @@ +package com.lagradost.cloudstream4.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("CheckReturnValue") +public val palette: ImageVector + get() { + if (_palette != null) { + return _palette!! + } + _palette = + ImageVector.Builder( + name = "palette", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = PathFillType.Companion.NonZero, + ) { + moveTo(12f, 22f) + quadTo(9.95f, 22f, 8.13f, 21.21f) + quadTo(6.3f, 20.43f, 4.94f, 19.06f) + quadTo(3.58f, 17.7f, 2.79f, 15.88f) + reflectiveQuadTo(2f, 12f) + quadTo(2f, 9.92f, 2.81f, 8.1f) + quadTo(3.63f, 6.27f, 5.01f, 4.93f) + quadTo(6.4f, 3.57f, 8.25f, 2.79f) + reflectiveQuadTo(12.2f, 2f) + quadToRelative(2f, 0f, 3.78f, 0.69f) + reflectiveQuadToRelative(3.11f, 1.9f) + reflectiveQuadToRelative(2.13f, 2.88f) + quadTo(22f, 9.13f, 22f, 11.05f) + quadToRelative(0f, 2.88f, -1.75f, 4.41f) + reflectiveQuadTo(16f, 17f) + horizontalLineTo(14.15f) + quadToRelative(-0.22f, 0f, -0.31f, 0.13f) + reflectiveQuadTo(13.75f, 17.4f) + quadToRelative(0f, 0.3f, 0.38f, 0.86f) + reflectiveQuadToRelative(0.38f, 1.29f) + quadToRelative(0f, 1.25f, -0.69f, 1.85f) + reflectiveQuadTo(12f, 22f) + close() + moveTo(12f, 12f) + close() + moveTo(7.58f, 12.58f) + quadTo(8f, 12.15f, 8f, 11.5f) + reflectiveQuadTo(7.58f, 10.43f) + reflectiveQuadTo(6.5f, 10f) + reflectiveQuadTo(5.43f, 10.43f) + reflectiveQuadTo(5f, 11.5f) + reflectiveQuadToRelative(0.43f, 1.07f) + reflectiveQuadTo(6.5f, 13f) + reflectiveQuadTo(7.58f, 12.58f) + close() + moveToRelative(3f, -4f) + quadTo(11f, 8.15f, 11f, 7.5f) + reflectiveQuadTo(10.58f, 6.43f) + reflectiveQuadTo(9.5f, 6f) + reflectiveQuadTo(8.43f, 6.43f) + reflectiveQuadTo(8f, 7.5f) + reflectiveQuadTo(8.43f, 8.57f) + reflectiveQuadTo(9.5f, 9f) + reflectiveQuadTo(10.58f, 8.57f) + close() + moveToRelative(5f, 0f) + quadTo(16f, 8.15f, 16f, 7.5f) + reflectiveQuadTo(15.58f, 6.43f) + reflectiveQuadTo(14.5f, 6f) + reflectiveQuadTo(13.43f, 6.43f) + reflectiveQuadTo(13f, 7.5f) + reflectiveQuadToRelative(0.43f, 1.07f) + reflectiveQuadTo(14.5f, 9f) + reflectiveQuadTo(15.58f, 8.57f) + close() + moveToRelative(3f, 4f) + quadTo(19f, 12.15f, 19f, 11.5f) + reflectiveQuadTo(18.58f, 10.43f) + reflectiveQuadTo(17.5f, 10f) + reflectiveQuadToRelative(-1.07f, 0.42f) + reflectiveQuadTo(16f, 11.5f) + reflectiveQuadToRelative(0.43f, 1.07f) + reflectiveQuadTo(17.5f, 13f) + reflectiveQuadToRelative(1.07f, -0.43f) + close() + moveTo(12f, 20f) + quadToRelative(0.23f, 0f, 0.36f, -0.13f) + reflectiveQuadTo(12.5f, 19.55f) + quadToRelative(0f, -0.35f, -0.38f, -0.82f) + reflectiveQuadTo(11.75f, 17.3f) + quadToRelative(0f, -1.05f, 0.73f, -1.68f) + reflectiveQuadTo(14.25f, 15f) + horizontalLineTo(16f) + quadToRelative(1.65f, 0f, 2.82f, -0.96f) + reflectiveQuadTo(20f, 11.05f) + quadTo(20f, 8.02f, 17.69f, 6.01f) + reflectiveQuadTo(12.2f, 4f) + quadTo(8.8f, 4f, 6.4f, 6.32f) + reflectiveQuadTo(4f, 12f) + quadToRelative(0f, 3.32f, 2.34f, 5.66f) + reflectiveQuadTo(12f, 20f) + close() + } + } + .build() + return _palette!! + } + +private var _palette: ImageVector? = null diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/PlayCircle.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/PlayCircle.kt new file mode 100644 index 00000000000..da6bb9960f1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/PlayCircle.kt @@ -0,0 +1,79 @@ +package com.lagradost.cloudstream4.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("CheckReturnValue") +public val play_circle: ImageVector + get() { + if (_play_circle != null) { + return _play_circle!! + } + _play_circle = + ImageVector.Builder( + name = "play_circle", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = PathFillType.Companion.NonZero, + ) { + moveTo(9.5f, 16.5f) + lineToRelative(7f, -4.5f) + lineTo(9.5f, 7.5f) + verticalLineToRelative(9f) + close() + moveTo(12f, 22f) + quadTo(9.93f, 22f, 8.1f, 21.21f) + quadTo(6.28f, 20.43f, 4.93f, 19.08f) + quadTo(3.58f, 17.73f, 2.79f, 15.9f) + reflectiveQuadTo(2f, 12f) + quadTo(2f, 9.92f, 2.79f, 8.1f) + quadTo(3.58f, 6.27f, 4.93f, 4.93f) + quadTo(6.28f, 3.57f, 8.1f, 2.79f) + quadTo(9.93f, 2f, 12f, 2f) + reflectiveQuadToRelative(3.9f, 0.79f) + reflectiveQuadToRelative(3.17f, 2.14f) + quadToRelative(1.35f, 1.35f, 2.14f, 3.17f) + quadTo(22f, 9.92f, 22f, 12f) + reflectiveQuadToRelative(-0.79f, 3.9f) + reflectiveQuadToRelative(-2.14f, 3.17f) + quadToRelative(-1.35f, 1.35f, -3.17f, 2.14f) + reflectiveQuadTo(12f, 22f) + close() + moveToRelative(0f, -2f) + quadToRelative(3.35f, 0f, 5.68f, -2.32f) + reflectiveQuadTo(20f, 12f) + reflectiveQuadTo(17.68f, 6.32f) + reflectiveQuadTo(12f, 4f) + reflectiveQuadTo(6.33f, 6.32f) + reflectiveQuadTo(4f, 12f) + reflectiveQuadToRelative(2.33f, 5.68f) + reflectiveQuadTo(12f, 20f) + close() + moveToRelative(0f, -8f) + close() + } + } + .build() + return _play_circle!! + } + +private var _play_circle: ImageVector? = null diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Storage.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Storage.kt new file mode 100644 index 00000000000..c5fcf9fca53 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Storage.kt @@ -0,0 +1,80 @@ +package com.lagradost.cloudstream4.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("CheckReturnValue") +public val storage: ImageVector + get() { + if (_storage != null) { + return _storage!! + } + _storage = + ImageVector.Builder( + name = "storage", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = PathFillType.Companion.NonZero, + ) { + moveTo(3f, 20f) + verticalLineTo(16f) + horizontalLineTo(21f) + verticalLineToRelative(4f) + horizontalLineTo(3f) + close() + moveTo(5f, 19f) + horizontalLineTo(7f) + verticalLineTo(17f) + horizontalLineTo(5f) + verticalLineToRelative(2f) + close() + moveTo(3f, 8f) + verticalLineTo(4f) + horizontalLineTo(21f) + verticalLineTo(8f) + horizontalLineTo(3f) + close() + moveTo(5f, 7f) + horizontalLineTo(7f) + verticalLineTo(5f) + horizontalLineTo(5f) + verticalLineTo(7f) + close() + moveTo(3f, 14f) + verticalLineTo(10f) + horizontalLineTo(21f) + verticalLineToRelative(4f) + horizontalLineTo(3f) + close() + moveTo(5f, 13f) + horizontalLineTo(7f) + verticalLineTo(11f) + horizontalLineTo(5f) + verticalLineToRelative(2f) + close() + } + } + .build() + return _storage!! + } + +private var _storage: ImageVector? = null diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Tune.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Tune.kt new file mode 100644 index 00000000000..97969fd9422 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/icons/Tune.kt @@ -0,0 +1,92 @@ +package com.lagradost.cloudstream4.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("CheckReturnValue") +public val tune: ImageVector + get() { + if (_tune != null) { + return _tune!! + } + _tune = + ImageVector.Builder( + name = "tune", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ) + .apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1f, + stroke = null, + strokeAlpha = 1f, + strokeLineWidth = 1f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Bevel, + strokeLineMiter = 1f, + pathFillType = PathFillType.Companion.NonZero, + ) { + moveTo(11f, 21f) + verticalLineTo(15f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineToRelative(8f) + verticalLineToRelative(2f) + horizontalLineTo(13f) + verticalLineToRelative(2f) + horizontalLineTo(11f) + close() + moveTo(3f, 19f) + verticalLineTo(17f) + horizontalLineTo(9f) + verticalLineToRelative(2f) + horizontalLineTo(3f) + close() + moveTo(7f, 15f) + verticalLineTo(13f) + horizontalLineTo(3f) + verticalLineTo(11f) + horizontalLineTo(7f) + verticalLineTo(9f) + horizontalLineTo(9f) + verticalLineToRelative(6f) + horizontalLineTo(7f) + close() + moveToRelative(4f, -2f) + verticalLineTo(11f) + horizontalLineTo(21f) + verticalLineToRelative(2f) + horizontalLineTo(11f) + close() + moveTo(15f, 9f) + verticalLineTo(3f) + horizontalLineToRelative(2f) + verticalLineTo(5f) + horizontalLineToRelative(4f) + verticalLineTo(7f) + horizontalLineTo(17f) + verticalLineTo(9f) + horizontalLineTo(15f) + close() + moveTo(3f, 7f) + verticalLineTo(5f) + horizontalLineTo(13f) + verticalLineTo(7f) + horizontalLineTo(3f) + close() + } + } + .build() + return _tune!! + } + +private var _tune: ImageVector? = null diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/screens/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/screens/settings/SettingsScreen.kt new file mode 100644 index 00000000000..a13502da1c0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/screens/settings/SettingsScreen.kt @@ -0,0 +1,228 @@ +package com.lagradost.cloudstream4.compose.screens.settings + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.lagradost.cloudstream4.compose.components.ProfileImage +import com.lagradost.cloudstream4.compose.components.ProfilePicture +import com.lagradost.cloudstream4.compose.components.cloudStreamRipple +import com.lagradost.cloudstream4.compose.components.settings.SettingsItem +import com.lagradost.cloudstream4.compose.icons.account_circle +import com.lagradost.cloudstream4.compose.icons.extension +import com.lagradost.cloudstream4.compose.icons.mobile_arrow_down +import com.lagradost.cloudstream4.compose.icons.palette +import com.lagradost.cloudstream4.compose.icons.play_circle +import com.lagradost.cloudstream4.compose.icons.storage +import com.lagradost.cloudstream4.compose.icons.tune +import com.lagradost.cloudstream4.compose.theme.CloudStreamTheme +import com.lagradost.cloudstream4.generated.resources.Res +import com.lagradost.cloudstream4.generated.resources.category_accounts +import com.lagradost.cloudstream4.generated.resources.category_accounts_subtitle +import com.lagradost.cloudstream4.generated.resources.category_extensions +import com.lagradost.cloudstream4.generated.resources.category_extensions_subtitle +import com.lagradost.cloudstream4.generated.resources.category_general +import com.lagradost.cloudstream4.generated.resources.category_general_subtitle +import com.lagradost.cloudstream4.generated.resources.category_layout +import com.lagradost.cloudstream4.generated.resources.category_layout_subtitle +import com.lagradost.cloudstream4.generated.resources.category_player +import com.lagradost.cloudstream4.generated.resources.category_player_subtitle +import com.lagradost.cloudstream4.generated.resources.category_providers +import com.lagradost.cloudstream4.generated.resources.category_providers_subtitle +import com.lagradost.cloudstream4.generated.resources.category_updates +import com.lagradost.cloudstream4.generated.resources.category_updates_subtitle +import com.lagradost.cloudstream4.utils.DeviceLayout +import org.jetbrains.compose.resources.stringResource + +data class SettingsProfileState( + val name: String, + val profilePictureUrl: String? = null, + val profileImage: ProfileImage = ProfileImage.DARK_BLUE, +) + +data class SettingsVersionState( + val appVersion: String, + val commitHash: String, + val buildDate: String, +) + +enum class SettingsCategory { + GENERAL, + PLAYER, + PROVIDERS, + LAYOUT, + UPDATES, + ACCOUNTS, + EXTENSIONS, +} + +@Composable +fun SettingsCategory.label(): String = stringResource( + when (this) { + SettingsCategory.GENERAL -> Res.string.category_general + SettingsCategory.PLAYER -> Res.string.category_player + SettingsCategory.PROVIDERS -> Res.string.category_providers + SettingsCategory.LAYOUT -> Res.string.category_layout + SettingsCategory.UPDATES -> Res.string.category_updates + SettingsCategory.ACCOUNTS -> Res.string.category_accounts + SettingsCategory.EXTENSIONS -> Res.string.category_extensions + } +) + +@Composable +fun SettingsCategory.subtitle(): String = stringResource( + when (this) { + SettingsCategory.GENERAL -> Res.string.category_general_subtitle + SettingsCategory.PLAYER -> Res.string.category_player_subtitle + SettingsCategory.PROVIDERS -> Res.string.category_providers_subtitle + SettingsCategory.LAYOUT -> Res.string.category_layout_subtitle + SettingsCategory.UPDATES -> Res.string.category_updates_subtitle + SettingsCategory.ACCOUNTS -> Res.string.category_accounts_subtitle + SettingsCategory.EXTENSIONS -> Res.string.category_extensions_subtitle + } +) + +private fun SettingsCategory.icon(): ImageVector = when (this) { + SettingsCategory.GENERAL -> tune + SettingsCategory.PLAYER -> play_circle + SettingsCategory.PROVIDERS -> storage + SettingsCategory.LAYOUT -> palette + SettingsCategory.UPDATES -> mobile_arrow_down + SettingsCategory.ACCOUNTS -> account_circle + SettingsCategory.EXTENSIONS -> extension +} + +@Composable +fun SettingsScreen( + profile: SettingsProfileState, + version: SettingsVersionState, + onNavigate: (SettingsCategory) -> Unit, + onVersionLongClick: () -> Unit = {}, +) { + val colors = CloudStreamTheme.colors + val isTV = remember { DeviceLayout.isLayout(DeviceLayout.TV) } + val firstItemFocusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + if (isTV) firstItemFocusRequester.requestFocus() + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(colors.background) + .windowInsetsPadding(WindowInsets.statusBars) + .then( + if (isTV) Modifier.windowInsetsPadding( + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) + ) else Modifier + ), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + SettingsProfileHeader(profile) + + SettingsCategory.entries.forEachIndexed { index, category -> + SettingsItem( + title = category.label(), + subtitle = category.subtitle(), + icon = category.icon(), + focusRequester = if (index == 0) firstItemFocusRequester else null, + onClick = { onNavigate(category) }, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsVersionFooter(version = version, onLongClick = onVersionLongClick) + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Composable +private fun SettingsProfileHeader(profile: SettingsProfileState) { + val colors = CloudStreamTheme.colors + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ProfilePicture( + profileImage = profile.profileImage, + profilePictureUrl = profile.profilePictureUrl, + size = 50.dp, + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Text( + text = profile.name, + color = colors.onBackground, + style = MaterialTheme.typography.bodyLarge, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun SettingsVersionFooter(version: SettingsVersionState, onLongClick: () -> Unit) { + val interactionSource = remember { MutableInteractionSource() } + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + interactionSource = interactionSource, + indication = null, + onLongClick = onLongClick, + onClick = {}, + ) + .cloudStreamRipple(interactionSource) + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + VersionChip(version.appVersion) + VersionDot() + VersionChip(version.commitHash) + VersionDot() + VersionChip(version.buildDate) + } +} + +@Composable +private fun VersionChip(text: String) { + Text( + text = text, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + color = CloudStreamTheme.colors.onBackground.copy(alpha = 0.6f), + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun VersionDot() { + Text( + text = "•", + color = CloudStreamTheme.colors.onBackground.copy(alpha = 0.6f), + style = MaterialTheme.typography.bodySmall, + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamColorScheme.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamColorScheme.kt new file mode 100644 index 00000000000..b4c6d4bcb18 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamColorScheme.kt @@ -0,0 +1,137 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color + +/** + * Maps to the XML custom attrs declared in attrs.xml: + * TODO: Remove this comment when we migrate fully + * and attrs.xml will no longer be used at all. + * + * | XML ?attr | Here | + * |--------------------------|--------------------| + * | primaryBlackBackground | [background] | + * | primaryGrayBackground | [surfaceVariant] | + * | iconGrayBackground | [surface] | + * | boxItemBackground | [surfaceContainer] | + * | textColor | [onBackground] | + * | grayTextColor | [onSurfaceVariant] | + * | iconColor | [icon] | + * | colorPrimary | [primary] | + * | colorOngoing | [ongoing] | + * + * All fields are [MutableState] so Compose recomposes automatically + * if the scheme is swapped at runtime (e.g. user changes theme without restart). + */ +@Stable +class CloudStreamColorScheme( + background: Color, + surfaceVariant: Color, + surface: Color, + surfaceContainer: Color, + onBackground: Color, + onSurfaceVariant: Color, + icon: Color, + primary: Color, + ongoing: Color, + isLight: Boolean, +) { + var background by mutableStateOf(background, structuralEqualityPolicy()) + var surfaceVariant by mutableStateOf(surfaceVariant, structuralEqualityPolicy()) + var surface by mutableStateOf(surface, structuralEqualityPolicy()) + var surfaceContainer by mutableStateOf(surfaceContainer, structuralEqualityPolicy()) + var onBackground by mutableStateOf(onBackground, structuralEqualityPolicy()) + var onSurfaceVariant by mutableStateOf(onSurfaceVariant, structuralEqualityPolicy()) + var icon by mutableStateOf(icon, structuralEqualityPolicy()) + var primary by mutableStateOf(primary, structuralEqualityPolicy()) + var ongoing by mutableStateOf(ongoing, structuralEqualityPolicy()) + var isLight by mutableStateOf(isLight, structuralEqualityPolicy()) + + fun copy( + background: Color = this.background, + surfaceVariant: Color = this.surfaceVariant, + surface: Color = this.surface, + surfaceContainer: Color = this.surfaceContainer, + onBackground: Color = this.onBackground, + onSurfaceVariant: Color = this.onSurfaceVariant, + icon: Color = this.icon, + primary: Color = this.primary, + ongoing: Color = this.ongoing, + isLight: Boolean = this.isLight, + ) = CloudStreamColorScheme( + background, surfaceVariant, surface, surfaceContainer, + onBackground, onSurfaceVariant, icon, primary, ongoing, isLight, + ) +} + +internal fun darkScheme() = CloudStreamColorScheme( + background = CloudStreamPalette.DarkBlackBg, + surfaceVariant = CloudStreamPalette.DarkPrimaryGrayBg, + surface = CloudStreamPalette.DarkIconGrayBg, + surfaceContainer = CloudStreamPalette.DarkBoxItemBg, + onBackground = CloudStreamPalette.DarkText, + onSurfaceVariant = CloudStreamPalette.DarkGrayText, + icon = CloudStreamPalette.DarkIcon, + primary = CloudStreamPalette.Primary, + ongoing = CloudStreamPalette.Ongoing, + isLight = false, +) + +internal fun amoledScheme() = darkScheme().copy( + background = CloudStreamPalette.AmoledBlack, + surface = CloudStreamPalette.AmoledBlack, + surfaceVariant = CloudStreamPalette.AmoledBlack, + surfaceContainer = CloudStreamPalette.AmoledBlack, +) + +internal fun lightScheme() = CloudStreamColorScheme( + background = CloudStreamPalette.LightBlackBg, + surfaceVariant = CloudStreamPalette.LightPrimaryGrayBg, + surface = CloudStreamPalette.LightIconGrayBg, + surfaceContainer = CloudStreamPalette.LightBoxItemBg, + onBackground = CloudStreamPalette.LightText, + onSurfaceVariant = CloudStreamPalette.LightGrayText, + icon = CloudStreamPalette.LightIcon, + primary = CloudStreamPalette.Primary, + ongoing = CloudStreamPalette.Ongoing, + isLight = true, +) + +internal fun draculaScheme() = CloudStreamColorScheme( + background = CloudStreamPalette.DraculaBlackBg, + surfaceVariant = CloudStreamPalette.DraculaPrimaryGrayBg, + surface = CloudStreamPalette.DraculaIconGrayBg, + surfaceContainer = CloudStreamPalette.DraculaBoxItemBg, + onBackground = CloudStreamPalette.DraculaText, + onSurfaceVariant = CloudStreamPalette.DraculaGrayText, + icon = CloudStreamPalette.DraculaIcon, + primary = CloudStreamPalette.Primary, + ongoing = CloudStreamPalette.Ongoing, + isLight = false, +) + +internal fun lavenderScheme() = CloudStreamColorScheme( + background = CloudStreamPalette.LavenderBlackBg, + surfaceVariant = CloudStreamPalette.LavenderPrimaryGrayBg, + surface = CloudStreamPalette.LavenderIconGrayBg, + surfaceContainer = CloudStreamPalette.LavenderBoxItemBg, + onBackground = CloudStreamPalette.LavenderText, + onSurfaceVariant = CloudStreamPalette.LavenderGrayText, + icon = CloudStreamPalette.LavenderIcon, + primary = CloudStreamPalette.Primary, + ongoing = CloudStreamPalette.Ongoing, + isLight = true, +) + +internal fun silentBlueScheme() = CloudStreamColorScheme( + background = CloudStreamPalette.SilentBlueBlackBg, + surfaceVariant = CloudStreamPalette.SilentBluePrimaryGrayBg, + surface = CloudStreamPalette.SilentBlueIconGrayBg, + surfaceContainer = CloudStreamPalette.SilentBlueBoxItemBg, + onBackground = CloudStreamPalette.SilentBlueText, + onSurfaceVariant = CloudStreamPalette.SilentBlueGrayText, + icon = CloudStreamPalette.SilentBlueIcon, + primary = CloudStreamPalette.Primary, + ongoing = CloudStreamPalette.Ongoing, + isLight = false, +) diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamPalette.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamPalette.kt new file mode 100644 index 00000000000..7f84f637c8e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamPalette.kt @@ -0,0 +1,58 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.ui.graphics.Color + +internal object CloudStreamPalette { + // Default dark (AppTheme / Black) + val Primary = Color(0xFF3D50FA) + val PrimaryDark = Color(0xFF3700B3) + val Ongoing = Color(0xFFF53B66) + + val DarkPrimaryGrayBg = Color(0xFF2B2C30) + val DarkBlackBg = Color(0xFF111111) + val DarkIconGrayBg = Color(0xFF1C1C20) + val DarkBoxItemBg = Color(0xFF161616) + val DarkText = Color(0xFFE9EAEE) + val DarkGrayText = Color(0xFF9BA0A4) + val DarkIcon = Color(0xFF9BA0A6) + + // Amoled + val AmoledBlack = Color(0xFF000000) + val AmoledNearBlack = Color(0xFF111111) + + // Light + val LightPrimaryGrayBg = Color(0xFFF1F1F1) + val LightBlackBg = Color(0xFFFFFFFF) + val LightIconGrayBg = Color(0xFFEEEEEE) + val LightBoxItemBg = Color(0xFFEEEEEE) + val LightText = Color(0xFF202125) + val LightGrayText = Color(0xFF5F6267) + val LightIcon = Color(0xFF5F6267) + + // Dracula + val DraculaPrimaryGrayBg = Color(0xFF414450) + val DraculaBlackBg = Color(0xFF282A36) + val DraculaIconGrayBg = Color(0xFF44475A) + val DraculaBoxItemBg = Color(0xFF373844) + val DraculaText = Color(0xFFF8F8F2) + val DraculaGrayText = Color(0xFF6272A4) + val DraculaIcon = Color(0xFF6272A4) + + // Lavender Dreams + val LavenderPrimaryGrayBg = Color(0xFFF7EEFC) + val LavenderBlackBg = Color(0xFFFDF0FB) + val LavenderIconGrayBg = Color(0xFFB794F6) + val LavenderBoxItemBg = Color(0xFFF8F5FF) + val LavenderText = Color(0xFF2D1B47) + val LavenderGrayText = Color(0xFF9AB3FF) + val LavenderIcon = Color(0xFF7C3AED) + + // Silent Blue + val SilentBluePrimaryGrayBg = Color(0xFF282F49) + val SilentBlueBlackBg = Color(0xFF151A30) + val SilentBlueIconGrayBg = Color(0xFF3A446A) + val SilentBlueBoxItemBg = Color(0xFF3A446A) + val SilentBlueText = Color(0xFFE0E1F3) + val SilentBlueGrayText = Color(0xFF7B83B0) + val SilentBlueIcon = Color(0xFF7B83B0) +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamPrimaryColor.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamPrimaryColor.kt new file mode 100644 index 00000000000..459a9ddaf88 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamPrimaryColor.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.ui.graphics.Color + +enum class CloudStreamPrimaryColor(val color: Color) { + NORMAL (Color(0xFF3D50FA)), + BLUE (Color(0xFF5664B7)), + PURPLE (Color(0xFF6200EA)), + GREEN (Color(0xFF00BFA5)), + GREEN_APPLE (Color(0xFF48E484)), + RED (Color(0xFFD50000)), + BANANA (Color(0xFFE4D448)), + PARTY (Color(0xFFEA596E)), + PINK (Color(0xFFFF1493)), + CARNATION_PINK (Color(0xFFBD5DA5)), + MAROON (Color(0xFF451010)), + DARK_GREEN (Color(0xFF004500)), + NAVY_BLUE (Color(0xFF000080)), + GREY (Color(0xFF515151)), + WHITE (Color(0xFFFFFFFF)), + BROWN (Color(0xFF622C00)), + ORANGE (Color(0xFFCE8500)), + DANDELION_YELLOW (Color(0xFFF5BB00)), + COOL_BLUE (Color(0xFF408CAC)), + LAVENDER (Color(0xFF6F55AF)), + DYNAMIC (Color(0xFF3D50FA)), + DYNAMIC_TWO (Color(0xFF3D50FA)), +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamTheme.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamTheme.kt new file mode 100644 index 00000000000..03cee9b256f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamTheme.kt @@ -0,0 +1,82 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color + +val LocalCloudStreamColors = staticCompositionLocalOf { darkScheme() } + +object CloudStreamTheme { + val colors: CloudStreamColorScheme + @Composable + @ReadOnlyComposable + get() = LocalCloudStreamColors.current +} + +private fun CloudStreamColorScheme.toMaterial3ColorScheme() = if (isLight) { + lightColorScheme( + primary = primary, + background = background, + surface = surface, + surfaceVariant = surfaceVariant, + surfaceContainer = surfaceContainer, + onBackground = onBackground, + onSurface = onBackground, + onSurfaceVariant = onSurfaceVariant, + onPrimary = Color.White, + ) +} else { + darkColorScheme( + primary = primary, + background = background, + surface = surface, + surfaceVariant = surfaceVariant, + surfaceContainer = surfaceContainer, + onBackground = onBackground, + onSurface = onBackground, + onSurfaceVariant = onSurfaceVariant, + onPrimary = Color.White, + ) +} + +@Composable +fun CloudStreamTheme( + mode: CloudStreamThemeMode = CloudStreamThemeMode.FollowSystem, + primaryColor: CloudStreamPrimaryColor = CloudStreamPrimaryColor.NORMAL, + content: @Composable () -> Unit, +) { + val systemDark = isSystemInDarkTheme() + val dynamicTheme = resolveDynamicTheme() + + val dynamicPrimary = resolveDynamicPrimaryColor() + val dynamicSecondary = resolveDynamicSecondaryColor() + + val csColors = remember(mode, primaryColor, systemDark, dynamicTheme, dynamicPrimary, dynamicSecondary) { + val base = when (mode) { + CloudStreamThemeMode.Dark -> darkScheme() + CloudStreamThemeMode.Amoled -> amoledScheme() + CloudStreamThemeMode.Light -> lightScheme() + CloudStreamThemeMode.Dracula -> draculaScheme() + CloudStreamThemeMode.Lavender -> lavenderScheme() + CloudStreamThemeMode.SilentBlue -> silentBlueScheme() + CloudStreamThemeMode.FollowSystem -> if (systemDark) darkScheme() else lightScheme() + CloudStreamThemeMode.Dynamic -> dynamicTheme + } + when { + mode == CloudStreamThemeMode.Dynamic -> base + primaryColor == CloudStreamPrimaryColor.DYNAMIC -> base.copy(primary = dynamicPrimary) + primaryColor == CloudStreamPrimaryColor.DYNAMIC_TWO -> base.copy(primary = dynamicSecondary) + else -> base.copy(primary = primaryColor.color) + } + } + + CompositionLocalProvider(LocalCloudStreamColors provides csColors) { + MaterialTheme( + colorScheme = csColors.toMaterial3ColorScheme(), + content = content, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamThemeMode.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamThemeMode.kt new file mode 100644 index 00000000000..3fdebc4f13b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamThemeMode.kt @@ -0,0 +1,23 @@ +package com.lagradost.cloudstream4.compose.theme + +enum class CloudStreamThemeMode { + /** "Black" standard dark, #111111 backgrounds */ + Dark, + /** "Amoled" / "AmoledLight" pure black (#000000) */ + Amoled, + /** "Light" white/gray backgrounds, dark text */ + Light, + /** "Dracula" */ + Dracula, + /** "Lavender" */ + Lavender, + /** "SilentBlue" */ + SilentBlue, + /** "System" resolved on each platform via [isSystemInDarkTheme] */ + FollowSystem, + /** + * Uses platform dynamic color system, Material You on Android 12+, + * falls back to [Dark] on unsupported platforms. + */ + Dynamic, +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicPrimaryColor.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicPrimaryColor.kt new file mode 100644 index 00000000000..5a171334023 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicPrimaryColor.kt @@ -0,0 +1,10 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +expect fun resolveDynamicPrimaryColor(): Color + +@Composable +expect fun resolveDynamicSecondaryColor(): Color diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.kt new file mode 100644 index 00000000000..56c630d1be9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.kt @@ -0,0 +1,6 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.runtime.Composable + +@Composable +expect fun resolveDynamicTheme(): CloudStreamColorScheme diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/preferences/PreferenceDefaults.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/preferences/PreferenceDefaults.kt new file mode 100644 index 00000000000..a96a2c8bae8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/preferences/PreferenceDefaults.kt @@ -0,0 +1,7 @@ +package com.lagradost.cloudstream4.preferences + +object PreferenceDefaults { + const val APP_THEME = "AmoledLight" + const val PRIMARY_COLOR = "Normal" + const val APP_LAYOUT = -1 // Auto +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/preferences/PreferenceKeys.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/preferences/PreferenceKeys.kt new file mode 100644 index 00000000000..2b42763d50b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/preferences/PreferenceKeys.kt @@ -0,0 +1,7 @@ +package com.lagradost.cloudstream4.preferences + +object PreferenceKeys { + const val APP_LAYOUT = "app_layout_key" + const val APP_THEME = "app_theme_key" + const val PRIMARY_COLOR = "primary_color_key" +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.kt new file mode 100644 index 00000000000..aeb15944fc1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.kt @@ -0,0 +1,11 @@ +package com.lagradost.cloudstream4.utils + +internal expect object DeviceInfo { + fun getDeviceType(): DeviceType + fun isLandscape(): Boolean + fun getLayoutPreference(): Int +} + +enum class DeviceType { + PHONE, TV, EMULATOR, COMPUTER +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/utils/DeviceLayout.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/utils/DeviceLayout.kt new file mode 100644 index 00000000000..6d82b904780 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/utils/DeviceLayout.kt @@ -0,0 +1,53 @@ +package com.lagradost.cloudstream4.utils + +object DeviceLayout { + const val PHONE: Int = 0b00001 + const val TV: Int = 0b00010 + const val EMULATOR: Int = 0b00100 + const val COMPUTER: Int = 0b01000 + + private var layoutId = -1 + // TODO when fully on Compose + // private val layoutId: Int get() = resolveLayout() + + /** + * Returns true if the layout is any of the flags, so + * 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, or COMPUTER + */ + fun isLayout(flags: Int): Boolean = (layoutId and flags) != 0 + + /** Returns true if the current orientation is landscape. */ + fun isLandscape(): Boolean = + isLayout(TV or EMULATOR) || DeviceInfo.isLandscape() + + /** + * Updates the cached layout ID from preferences and device detection. + * + * TODO: Remove caching once fully migrated to Compose, layout will be read + * directly via [DeviceInfo] during composition where caching is handled by + * the Compose runtime. + */ + fun update() { + layoutId = resolveLayout() + } + + private fun resolveLayout(): Int { + return when (DeviceInfo.getLayoutPreference()) { + -1 -> when (DeviceInfo.getDeviceType()) { + DeviceType.COMPUTER -> COMPUTER + DeviceType.EMULATOR -> EMULATOR + DeviceType.PHONE -> PHONE + DeviceType.TV -> TV + } + 0 -> PHONE + 1 -> TV + 2 -> EMULATOR + else -> PHONE + } + } +} diff --git a/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicPrimaryColor.jvm.kt b/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicPrimaryColor.jvm.kt new file mode 100644 index 00000000000..cf6fef42679 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicPrimaryColor.jvm.kt @@ -0,0 +1,10 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +actual fun resolveDynamicPrimaryColor(): Color = CloudStreamPrimaryColor.NORMAL.color + +@Composable +actual fun resolveDynamicSecondaryColor(): Color = CloudStreamPrimaryColor.NORMAL.color diff --git a/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.jvm.kt b/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.jvm.kt new file mode 100644 index 00000000000..3be5a7e4f8c --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/compose/theme/DynamicTheme.jvm.kt @@ -0,0 +1,7 @@ +package com.lagradost.cloudstream4.compose.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +actual fun resolveDynamicTheme(): CloudStreamColorScheme = darkScheme() diff --git a/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.jvm.kt b/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.jvm.kt new file mode 100644 index 00000000000..93ffcdd0255 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.jvm.kt @@ -0,0 +1,18 @@ +package com.lagradost.cloudstream4.utils + +import java.awt.Toolkit + +internal actual object DeviceInfo { + actual fun getDeviceType(): DeviceType = DeviceType.COMPUTER + + actual fun isLandscape(): Boolean { + return try { + val screenSize = Toolkit.getDefaultToolkit().screenSize + screenSize.width > screenSize.height + } catch (_: Exception) { + true // Assume landscape as that is more likely on JVM + } + } + + actual fun getLayoutPreference(): Int = -1 +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a97145c3f81..4e722e3a559 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ # https://docs.gradle.org/current/userguide/plugins.html#sec:version_catalog_plugin_application # https://docs.gradle.org/current/userguide/dependency_versions.html#sec:strict-version [versions] -activityKtx = "1.13.0" +activity = "1.13.0" androidGradlePlugin = "9.1.1" animeDb = "1.0.2" annotation = "1.10.0" @@ -10,6 +10,8 @@ biometric = "1.4.0-alpha06" buildkonfigGradlePlugin = "0.18.0" coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later colorpicker = "6b46b49" +composeMaterial3 = "1.11.0-alpha07" +composeMultiplatform = "1.11.0" conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything constraintlayout = "2.2.1" coreKtx = "1.18.0" @@ -57,14 +59,21 @@ compileSdk = "36" targetSdk = "36" [libraries] -activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } +activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity" } anime-db = { module = "com.github.recloudstream:anime-db", version.ref = "animeDb" } annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } colorpicker = { module = "com.github.recloudstream:color-picker-android", version.ref = "colorpicker" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } +compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMaterial3" } +compose-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = "conscryptAndroid" } constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } core = { module = "androidx.test:core" } @@ -122,6 +131,8 @@ android-application = { id = "com.android.application", version.ref = "androidGr android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } android-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" } buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfigGradlePlugin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlinGradlePlugin" } +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaGradlePlugin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm" , version.ref = "kotlinGradlePlugin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlinGradlePlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 73bf5a1958b..df510d42fbb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,4 +18,4 @@ dependencyResolutionManagement { } rootProject.name = "CloudStream" -include(":app", ":library", ":docs") +include(":app", ":composeApp", ":library", ":docs")