From fbe276e20f9ba44f34f9737fddb60de77f3993ce Mon Sep 17 00:00:00 2001 From: michaelbel Date: Mon, 26 Feb 2024 11:56:59 +0300 Subject: [PATCH] Settings redesign --- .../michaelbel/movies/common/SealedString.kt | 3 + .../movies/common/appearance/FeedView.kt | 3 +- .../movies/common/gender/GrammaticalGender.kt | 4 +- .../movies/common/list/MovieList.kt | 4 +- .../common/localization/model/AppLanguage.kt | 4 +- .../movies/common/theme/AppTheme.kt | 3 +- .../movies/interactor/SettingsInteractor.kt | 2 + .../interactor/impl/SettingsInteractorImpl.kt | 3 + .../movies/repository/SettingsRepository.kt | 2 + .../repository/impl/SettingsRepositoryImpl.kt | 6 +- .../michaelbel/movies/ui/appicon/IconAlias.kt | 13 +- .../michaelbel/movies/ui/color/hct/Cam16.kt | 229 ++++++++ .../org/michaelbel/movies/ui/color/hct/Hct.kt | 59 ++ .../movies/ui/color/hct/HctSolver.kt | 551 ++++++++++++++++++ .../movies/ui/color/hct/ViewingConditions.kt | 93 +++ .../movies/ui/color/utils/ColorUtils.kt | 166 ++++++ .../movies/ui/color/utils/MathUtils.kt | 72 +++ .../movies/ui/color/utils/StringUtils.kt | 10 + .../michaelbel/movies/ui/icons/MoviesIcons.kt | 6 + .../ui/src/main/res/drawable/ic_github_24.xml | 11 + .../main/res/drawable/ic_google_play_24.xml | 11 + .../movies/settings/SettingsViewModel.kt | 2 + .../movies/settings/ktx/AppLanguageKtx.kt | 12 - .../movies/settings/ktx/FeedViewKtx.kt | 12 - .../settings/ktx/GrammaticalGenderKtx.kt | 14 - .../movies/settings/ktx/IconAliasKtx.kt | 21 - .../movies/settings/ktx/MovieListKtx.kt | 14 - .../movies/settings/ktx/SealedStringKtx.kt | 37 ++ .../movies/settings/ktx/SystemThemeKtx.kt | 14 - .../movies/settings/ui/AppIconBox.kt | 120 ---- .../movies/settings/ui/SettingsAppIconBox.kt | 146 ----- .../settings/ui/SettingsAppWidgetBox.kt | 87 --- .../settings/ui/SettingsAppearanceBox.kt | 123 ---- .../settings/ui/SettingsAppearanceDialog.kt | 157 ----- .../settings/ui/SettingsDynamicColorsBox.kt | 106 ---- .../movies/settings/ui/SettingsGenderBox.kt | 116 ---- .../settings/ui/SettingsGenderDialog.kt | 153 ----- .../movies/settings/ui/SettingsGithubBox.kt | 85 --- .../movies/settings/ui/SettingsLanguageBox.kt | 159 ----- .../settings/ui/SettingsMovieListBox.kt | 123 ---- .../settings/ui/SettingsMovieListDialog.kt | 161 ----- .../ui/SettingsPostNotificationsBox.kt | 109 ---- .../movies/settings/ui/SettingsReviewBox.kt | 75 --- .../settings/ui/SettingsScreenContent.kt | 291 +++++++-- .../movies/settings/ui/SettingsThemeBox.kt | 122 ---- .../movies/settings/ui/SettingsThemeDialog.kt | 155 ----- .../movies/settings/ui/SettingsToolbar.kt | 8 +- .../settings/ui/common/SettingAppIcon.kt | 124 ++++ .../settings/ui/common/SettingDialog.kt | 239 ++++++++ .../movies/settings/ui/common/SettingItem.kt | 134 +++++ .../settings/ui/common/SettingSwitchItem.kt | 209 +++++++ .../src/main/res/values-ru/strings.xml | 14 +- .../src/main/res/values/strings.xml | 10 +- 53 files changed, 2254 insertions(+), 2153 deletions(-) create mode 100644 core/common/src/main/kotlin/org/michaelbel/movies/common/SealedString.kt create mode 100644 core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Cam16.kt create mode 100644 core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Hct.kt create mode 100644 core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/HctSolver.kt create mode 100644 core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/ViewingConditions.kt create mode 100644 core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/ColorUtils.kt create mode 100644 core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/MathUtils.kt create mode 100644 core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/StringUtils.kt create mode 100644 core/ui/src/main/res/drawable/ic_github_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_google_play_24.xml delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/FeedViewKtx.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/GrammaticalGenderKtx.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/MovieListKtx.kt create mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SealedStringKtx.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/AppIconBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppIconBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppWidgetBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsDynamicColorsBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderDialog.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGithubBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsReviewBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt delete mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt create mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingAppIcon.kt create mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingDialog.kt create mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingItem.kt create mode 100644 feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingSwitchItem.kt diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/SealedString.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/SealedString.kt new file mode 100644 index 000000000..14327d722 --- /dev/null +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/SealedString.kt @@ -0,0 +1,3 @@ +package org.michaelbel.movies.common + +interface SealedString \ No newline at end of file diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/appearance/FeedView.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/appearance/FeedView.kt index 35896a5e1..ab3744a13 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/appearance/FeedView.kt +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/appearance/FeedView.kt @@ -1,8 +1,9 @@ package org.michaelbel.movies.common.appearance +import org.michaelbel.movies.common.SealedString import org.michaelbel.movies.common.appearance.exceptions.InvalidFeedViewException -sealed interface FeedView { +sealed interface FeedView: SealedString { data object FeedList: FeedView diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/gender/GrammaticalGender.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/gender/GrammaticalGender.kt index b59314e31..521f32c84 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/gender/GrammaticalGender.kt +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/gender/GrammaticalGender.kt @@ -1,11 +1,13 @@ package org.michaelbel.movies.common.gender import android.content.res.Configuration +import org.michaelbel.movies.common.SealedString import org.michaelbel.movies.common.gender.exceptions.InvalidGenderException sealed class GrammaticalGender( val value: Int -) { +): SealedString { + data object NotSpecified: GrammaticalGender(Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED) data object Neutral: GrammaticalGender(Configuration.GRAMMATICAL_GENDER_NEUTRAL) diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/list/MovieList.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/list/MovieList.kt index 0d50919af..b995b03b5 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/list/MovieList.kt +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/list/MovieList.kt @@ -1,10 +1,12 @@ package org.michaelbel.movies.common.list +import org.michaelbel.movies.common.SealedString import org.michaelbel.movies.common.list.exceptions.InvalidMovieListException sealed class MovieList( val name: String -) { +): SealedString { + data object NowPlaying: MovieList("now_playing") data object Popular: MovieList("popular") diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/localization/model/AppLanguage.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/localization/model/AppLanguage.kt index ca5bf95f7..c5b9c4b26 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/localization/model/AppLanguage.kt +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/localization/model/AppLanguage.kt @@ -1,10 +1,12 @@ package org.michaelbel.movies.common.localization.model +import org.michaelbel.movies.common.SealedString import org.michaelbel.movies.common.localization.exceptions.InvalidLocaleException sealed class AppLanguage( val code: String -) { +): SealedString { + data object English: AppLanguage("en") data object Russian: AppLanguage("ru") diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/theme/AppTheme.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/theme/AppTheme.kt index 4ef8be99b..38bcc88cd 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/theme/AppTheme.kt +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/theme/AppTheme.kt @@ -1,8 +1,9 @@ package org.michaelbel.movies.common.theme +import org.michaelbel.movies.common.SealedString import org.michaelbel.movies.common.theme.exceptions.InvalidThemeException -sealed interface AppTheme { +sealed interface AppTheme: SealedString { data object NightNo: AppTheme diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/SettingsInteractor.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/SettingsInteractor.kt index 57e941ab5..34320d958 100644 --- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/SettingsInteractor.kt +++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/SettingsInteractor.kt @@ -8,6 +8,8 @@ import org.michaelbel.movies.common.version.AppVersionData interface SettingsInteractor { + val isReviewFeatureEnabled: Boolean + val currentTheme: Flow val currentFeedView: Flow diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/impl/SettingsInteractorImpl.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/impl/SettingsInteractorImpl.kt index 6b2d7af41..46b16c674 100644 --- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/impl/SettingsInteractorImpl.kt +++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/impl/SettingsInteractorImpl.kt @@ -30,6 +30,9 @@ internal class SettingsInteractorImpl @Inject constructor( appService: AppService ): SettingsInteractor { + override val isReviewFeatureEnabled: Boolean + get() = settingsRepository.isReviewFeatureEnabled + override val currentTheme: Flow = settingsRepository.currentTheme override val currentFeedView: Flow = settingsRepository.currentFeedView diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/SettingsRepository.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/SettingsRepository.kt index a0761f976..c6f8f2e7a 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/SettingsRepository.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/SettingsRepository.kt @@ -8,6 +8,8 @@ import org.michaelbel.movies.common.version.AppVersionData interface SettingsRepository { + val isReviewFeatureEnabled: Boolean + val currentTheme: Flow val currentFeedView: Flow diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SettingsRepositoryImpl.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SettingsRepositoryImpl.kt index c37b85b04..de0df208e 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SettingsRepositoryImpl.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SettingsRepositoryImpl.kt @@ -14,6 +14,7 @@ import org.michaelbel.movies.common.list.MovieList import org.michaelbel.movies.common.theme.AppTheme import org.michaelbel.movies.common.version.AppVersionData import org.michaelbel.movies.persistence.datastore.MoviesPreferences +import org.michaelbel.movies.platform.Flavor import org.michaelbel.movies.platform.app.AppService import org.michaelbel.movies.repository.SettingsRepository import org.michaelbel.movies.repository.ktx.code @@ -23,9 +24,12 @@ import org.michaelbel.movies.repository.ktx.packageInfo internal class SettingsRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, private val preferences: MoviesPreferences, - appService: AppService + private val appService: AppService ): SettingsRepository { + override val isReviewFeatureEnabled: Boolean + get() = BuildConfig.BUILD_TYPE == "Release" && appService.flavor == Flavor.Gms + override val currentTheme: Flow = preferences.themeFlow.map { name -> AppTheme.transform(name ?: AppTheme.FollowSystem.toString()) } diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/IconAlias.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/IconAlias.kt index d656718d3..267d0df52 100644 --- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/IconAlias.kt +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/IconAlias.kt @@ -8,20 +8,15 @@ sealed class IconAlias( @DrawableRes val iconRes: Int ) { - data object Red: IconAlias(RED_ICON_KEY, R.drawable.ic_launcher_icon_red) + data object Red: IconAlias("RedIcon", R.drawable.ic_launcher_icon_red) - data object Purple: IconAlias(PURPLE_ICON_KEY, R.drawable.ic_launcher_icon_purple) + data object Purple: IconAlias("PurpleIcon", R.drawable.ic_launcher_icon_purple) - data object Brown: IconAlias(BROWN_ICON_KEY, R.drawable.ic_launcher_icon_brown) + data object Brown: IconAlias("BrownIcon", R.drawable.ic_launcher_icon_brown) - data object Amoled: IconAlias(AMOLED_ICON_KEY, R.drawable.ic_launcher_icon_amoled) + data object Amoled: IconAlias("AmoledIcon", R.drawable.ic_launcher_icon_amoled) companion object { - private const val RED_ICON_KEY = "RedIcon" - private const val PURPLE_ICON_KEY = "PurpleIcon" - private const val BROWN_ICON_KEY = "BrownIcon" - private const val AMOLED_ICON_KEY = "AmoledIcon" - val VALUES = listOf( Red, Purple, diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Cam16.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Cam16.kt new file mode 100644 index 000000000..9b0fbf469 --- /dev/null +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Cam16.kt @@ -0,0 +1,229 @@ +package org.michaelbel.movies.ui.color.hct + +import kotlin.math.abs +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.expm1 +import kotlin.math.hypot +import kotlin.math.ln1p +import kotlin.math.max +import kotlin.math.pow +import kotlin.math.sign +import kotlin.math.sin +import kotlin.math.sqrt +import org.michaelbel.movies.ui.color.utils.ColorUtils + +class Cam16 + +private constructor( + val hue: Double, + val chroma: Double, + val j: Double, + val q: Double, + val m: Double, + val s: Double, + val jstar: Double, + val astar: Double, + val bstar: Double +) { + + private val tempArray = doubleArrayOf(0.0, 0.0, 0.0) + + fun distance(other: Cam16): Double { + val dJ = jstar - other.jstar + val dA = astar - other.astar + val dB = bstar - other.bstar + val dEPrime = sqrt(dJ * dJ + dA * dA + dB * dB) + return 1.41 * dEPrime.pow(0.63) + } + + fun toInt(): Int { + return viewed(ViewingConditions.DEFAULT) + } + + fun viewed(viewingConditions: ViewingConditions): Int { + val xyz = xyzInViewingConditions(viewingConditions, tempArray) + return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]) + } + + fun xyzInViewingConditions( + viewingConditions: ViewingConditions, + returnArray: DoubleArray? + ): DoubleArray { + val alpha = if (chroma == 0.0 || j == 0.0) 0.0 else chroma / sqrt(j / 100.0) + val t = (alpha / (1.64 - 0.29.pow(viewingConditions.n)).pow(0.73)).pow(1.0 / 0.9) + val hRad = hue.toRadians() + + val eHue = 0.25 * (cos(hRad + 2.0) + 3.8) + val ac = (viewingConditions.aw * (j / 100.0).pow(1.0 / viewingConditions.c / viewingConditions.z)) + val p1 = eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb + val p2 = ac / viewingConditions.nbb + val hSin = sin(hRad) + val hCos = cos(hRad) + val gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin) + val a = gamma * hCos + val b = gamma * hSin + val rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0 + val gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0 + val bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0 + val rCBase = max(0.0, 27.13 * abs(rA) / (400.0 - abs(rA))) + val rC = sign(rA) * (100.0 / viewingConditions.fl) * rCBase.pow(1.0 / 0.42) + val gCBase = max(0.0, 27.13 * abs(gA) / (400.0 - abs(gA))) + val gC = sign(gA) * (100.0 / viewingConditions.fl) * gCBase.pow(1.0 / 0.42) + val bCBase = max(0.0, 27.13 * abs(bA) / (400.0 - abs(bA))) + val bC = sign(bA) * (100.0 / viewingConditions.fl) * bCBase.pow(1.0 / 0.42) + val rF = rC / viewingConditions.rgbD[0] + val gF = gC / viewingConditions.rgbD[1] + val bF = bC / viewingConditions.rgbD[2] + val matrix = CAM16RGB_TO_XYZ + val x = rF * matrix[0][0] + gF * matrix[0][1] + bF * matrix[0][2] + val y = rF * matrix[1][0] + gF * matrix[1][1] + bF * matrix[1][2] + val z = rF * matrix[2][0] + gF * matrix[2][1] + bF * matrix[2][2] + return if (returnArray != null) { + returnArray[0] = x + returnArray[1] = y + returnArray[2] = z + returnArray + } else { + doubleArrayOf(x, y, z) + } + } + + companion object { + val XYZ_TO_CAM16RGB = arrayOf( + doubleArrayOf(0.401288, 0.650173, -0.051461), + doubleArrayOf(-0.250268, 1.204414, 0.045854), + doubleArrayOf(-0.002079, 0.048952, 0.953127) + ) + + val CAM16RGB_TO_XYZ = arrayOf( + doubleArrayOf(1.8620678, -1.0112547, 0.14918678), + doubleArrayOf(0.38752654, 0.62144744, -0.00897398), + doubleArrayOf(-0.01584150, -0.03412294, 1.0499644) + ) + + fun fromInt(argb: Int): Cam16 { + return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT) + } + + fun fromIntInViewingConditions(argb: Int, viewingConditions: ViewingConditions): Cam16 { + val red = argb and 0x00ff0000 shr 16 + val green = argb and 0x0000ff00 shr 8 + val blue = argb and 0x000000ff + val redL = ColorUtils.linearized(red) + val greenL = ColorUtils.linearized(green) + val blueL = ColorUtils.linearized(blue) + val x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL + val y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL + val z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL + return fromXyzInViewingConditions(x, y, z, viewingConditions) + } + + fun fromXyzInViewingConditions( + x: Double, + y: Double, + z: Double, + viewingConditions: ViewingConditions + ): Cam16 { + val matrix = XYZ_TO_CAM16RGB + val rT = x * matrix[0][0] + y * matrix[0][1] + z * matrix[0][2] + val gT = x * matrix[1][0] + y * matrix[1][1] + z * matrix[1][2] + val bT = x * matrix[2][0] + y * matrix[2][1] + z * matrix[2][2] + + val rD = viewingConditions.rgbD[0] * rT + val gD = viewingConditions.rgbD[1] * gT + val bD = viewingConditions.rgbD[2] * bT + + val rAF = (viewingConditions.fl * abs(rD) / 100.0).pow(0.42) + val gAF = (viewingConditions.fl * abs(gD) / 100.0).pow(0.42) + val bAF = (viewingConditions.fl * abs(bD) / 100.0).pow(0.42) + val rA = sign(rD) * 400.0 * rAF / (rAF + 27.13) + val gA = sign(gD) * 400.0 * gAF / (gAF + 27.13) + val bA = sign(bD) * 400.0 * bAF / (bAF + 27.13) + + val a = (11.0 * rA + -12.0 * gA + bA) / 11.0 + val b = (rA + gA - 2.0 * bA) / 9.0 + + val u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0 + val p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0 + + val atan2 = atan2(b, a) + val atanDegrees = atan2.toDegrees() + val hue = if (atanDegrees < 0) atanDegrees + 360.0 else if (atanDegrees >= 360) atanDegrees - 360.0 else atanDegrees + val hueRadians = hue.toRadians() + + val ac = p2 * viewingConditions.nbb + + val j = (100.0 * (ac / viewingConditions.aw).pow(viewingConditions.c * viewingConditions.z)) + val q = ((4.0 / viewingConditions.c) * sqrt(j / 100.0) * (viewingConditions.aw + 4.0) * viewingConditions.flRoot) + + val huePrime = if (hue < 20.14) hue + 360 else hue + val eHue = 0.25 * (cos(huePrime.toRadians() + 2.0) + 3.8) + val p1 = 50000.0 / 13.0 * eHue * viewingConditions.nc * viewingConditions.ncb + val t = p1 * hypot(a, b) / (u + 0.305) + val alpha = (1.64 - 0.29.pow(viewingConditions.n)).pow(0.73) * t.pow(0.9) + + val c = alpha * sqrt(j / 100.0) + val m = c * viewingConditions.flRoot + val s = 50.0 * sqrt(alpha * viewingConditions.c / (viewingConditions.aw + 4.0)) + + val jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j) + val mstar = 1.0 / 0.0228 * ln1p(0.0228 * m) + val astar = mstar * cos(hueRadians) + val bstar = mstar * sin(hueRadians) + return Cam16(hue, c, j, q, m, s, jstar, astar, bstar) + } + + fun fromJch(j: Double, c: Double, h: Double): Cam16 { + return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT) + } + + private fun fromJchInViewingConditions( + j: Double, + c: Double, + h: Double, + viewingConditions: ViewingConditions + ): Cam16 { + val q = ((4.0 / viewingConditions.c) * sqrt(j / 100.0) * (viewingConditions.aw + 4.0) * viewingConditions.flRoot) + val m = c * viewingConditions.flRoot + val alpha = c / sqrt(j / 100.0) + val s = 50.0 * sqrt(alpha * viewingConditions.c / (viewingConditions.aw + 4.0)) + val hueRadians = h.toRadians() + val jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j) + val mstar = 1.0 / 0.0228 * ln1p(0.0228 * m) + val astar = mstar * cos(hueRadians) + val bstar = mstar * sin(hueRadians) + return Cam16(h, c, j, q, m, s, jstar, astar, bstar) + } + + fun fromUcs(jstar: Double, astar: Double, bstar: Double): Cam16 { + return fromUcsInViewingConditions( + jstar, + astar, + bstar, + ViewingConditions.DEFAULT + ) + } + + fun fromUcsInViewingConditions( + jstar: Double, + astar: Double, + bstar: Double, + viewingConditions: ViewingConditions + ): Cam16 { + val m = hypot(astar, bstar) + val m2 = expm1(m * 0.0228) / 0.0228 + val c = m2 / viewingConditions.flRoot + var h = atan2(bstar, astar) * (180.0 / kotlin.math.PI) + if (h < 0.0) { + h += 360.0 + } + val j = jstar / (1.0 - (jstar - 100.0) * 0.007) + return fromJchInViewingConditions(j, c, h, viewingConditions) + } + + private fun Double.toRadians() = this * kotlin.math.PI / 180.0 + + private fun Double.toDegrees() = this * 180.0 / kotlin.math.PI + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Hct.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Hct.kt new file mode 100644 index 000000000..9245e8d40 --- /dev/null +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/Hct.kt @@ -0,0 +1,59 @@ +package org.michaelbel.movies.ui.color.hct + +import org.michaelbel.movies.ui.color.utils.ColorUtils + +class Hct private constructor(argb: Int) { + var hue = 0.0 + private set + var chroma = 0.0 + private set + var tone = 0.0 + private set + var argb = 0 + + init { + setInternalState(argb) + } + + fun toInt(): Int { + return argb + } + + fun setHue(newHue: Double) { + setInternalState(HctSolver.solveToInt(newHue, chroma, tone)) + } + + fun setChroma(newChroma: Double) { + setInternalState(HctSolver.solveToInt(hue, newChroma, tone)) + } + + fun setTone(newTone: Double) { + setInternalState(HctSolver.solveToInt(hue, chroma, newTone)) + } + + fun inViewingConditions(vc: ViewingConditions): Hct { + val cam16: Cam16 = Cam16.fromInt(toInt()) + val viewedInVc = cam16.xyzInViewingConditions(vc, null) + val recastInVc: Cam16 = Cam16.fromXyzInViewingConditions(viewedInVc[0], viewedInVc[1], viewedInVc[2], ViewingConditions.DEFAULT) + return from(recastInVc.hue, recastInVc.chroma, ColorUtils.lstarFromY(viewedInVc[1])) + } + + private fun setInternalState(argb: Int) { + this.argb = argb + val cam: Cam16 = Cam16.fromInt(argb) + hue = cam.hue + chroma = cam.chroma + tone = ColorUtils.lstarFromArgb(argb) + } + + companion object { + fun from(hue: Double, chroma: Double, tone: Double): Hct { + val argb = HctSolver.solveToInt(hue, chroma, tone) + return Hct(argb) + } + + fun fromInt(argb: Int): Hct { + return Hct(argb) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/HctSolver.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/HctSolver.kt new file mode 100644 index 000000000..8d5098e57 --- /dev/null +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/HctSolver.kt @@ -0,0 +1,551 @@ +package org.michaelbel.movies.ui.color.hct + +import kotlin.math.abs +import kotlin.math.atan2 +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.floor +import kotlin.math.max +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt +import org.michaelbel.movies.ui.color.utils.ColorUtils +import org.michaelbel.movies.ui.color.utils.MathUtils + +object HctSolver { + val SCALED_DISCOUNT_FROM_LINRGB = arrayOf( + doubleArrayOf(0.001200833568784504, 0.002389694492170889, 0.0002795742885861124), + doubleArrayOf(0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398), + doubleArrayOf(0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076) + ) + val LINRGB_FROM_SCALED_DISCOUNT = arrayOf( + doubleArrayOf(1373.2198709594231, -1100.4251190754821, -7.278681089101213), + doubleArrayOf(-271.815969077903, 559.6580465940733, -32.46047482791194), + doubleArrayOf(1.9622899599665666, -57.173814538844006, 308.7233197812385) + ) + val Y_FROM_LINRGB = doubleArrayOf(0.2126, 0.7152, 0.0722) + val CRITICAL_PLANES = doubleArrayOf( + 0.015176349177441876, + 0.045529047532325624, + 0.07588174588720938, + 0.10623444424209313, + 0.13658714259697685, + 0.16693984095186062, + 0.19729253930674434, + 0.2276452376616281, + 0.2579979360165119, + 0.28835063437139563, + 0.3188300904430532, + 0.350925934958123, + 0.3848314933096426, + 0.42057480301049466, + 0.458183274052838, + 0.4976837250274023, + 0.5391024159806381, + 0.5824650784040898, + 0.6277969426914107, + 0.6751227633498623, + 0.7244668422128921, + 0.775853049866786, + 0.829304845476233, + 0.8848452951698498, + 0.942497089126609, + 1.0022825574869039, + 1.0642236851973577, + 1.1283421258858297, + 1.1946592148522128, + 1.2631959812511864, + 1.3339731595349034, + 1.407011200216447, + 1.4823302800086415, + 1.5599503113873272, + 1.6398909516233677, + 1.7221716113234105, + 1.8068114625156377, + 1.8938294463134073, + 1.9832442801866852, + 2.075074464868551, + 2.1693382909216234, + 2.2660538449872063, + 2.36523901573795, + 2.4669114995532007, + 2.5710888059345764, + 2.6777882626779785, + 2.7870270208169257, + 2.898822059350997, + 3.0131901897720907, + 3.1301480604002863, + 3.2497121605402226, + 3.3718988244681087, + 3.4967242352587946, + 3.624204428461639, + 3.754355295633311, + 3.887192587735158, + 4.022731918402185, + 4.160988767090289, + 4.301978482107941, + 4.445716283538092, + 4.592217266055746, + 4.741496401646282, + 4.893568542229298, + 5.048448422192488, + 5.20615066083972, + 5.3666897647573375, + 5.5300801301023865, + 5.696336044816294, + 5.865471690767354, + 6.037501145825082, + 6.212438385869475, + 6.390297286737924, + 6.571091626112461, + 6.7548350853498045, + 6.941541251256611, + 7.131223617812143, + 7.323895587840543, + 7.5195704746346665, + 7.7182615035334345, + 7.919981813454504, + 8.124744458384042, + 8.332562408825165, + 8.543448553206703, + 8.757415699253682, + 8.974476575321063, + 9.194643831691977, + 9.417930041841839, + 9.644347703669503, + 9.873909240696694, + 10.106627003236781, + 10.342513269534024, + 10.58158024687427, + 10.8238400726681, + 11.069304815507364, + 11.317986476196008, + 11.569896988756009, + 11.825048221409341, + 12.083451977536606, + 12.345119996613247, + 12.610063955123938, + 12.878295467455942, + 13.149826086772048, + 13.42466730586372, + 13.702830557985108, + 13.984327217668513, + 14.269168601521828, + 14.55736596900856, + 14.848930523210871, + 15.143873411576273, + 15.44220572664832, + 15.743938506781891, + 16.04908273684337, + 16.35764934889634, + 16.66964922287304, + 16.985093187232053, + 17.30399201960269, + 17.62635644741625, + 17.95219714852476, + 18.281524751807332, + 18.614349837764564, + 18.95068293910138, + 19.290534541298456, + 19.633915083172692, + 19.98083495742689, + 20.331304511189067, + 20.685334046541502, + 21.042933821039977, + 21.404114048223256, + 21.76888489811322, + 22.137256497705877, + 22.50923893145328, + 22.884842241736916, + 23.264076429332462, + 23.6469514538663, + 24.033477234264016, + 24.42366364919083, + 24.817520537484558, + 25.21505769858089, + 25.61628489293138, + 26.021211842414342, + 26.429848230738664, + 26.842203703840827, + 27.258287870275353, + 27.678110301598522, + 28.10168053274597, + 28.529008062403893, + 28.96010235337422, + 29.39497283293396, + 29.83362889318845, + 30.276079891419332, + 30.722335150426627, + 31.172403958865512, + 31.62629557157785, + 32.08401920991837, + 32.54558406207592, + 33.010999283389665, + 33.4802739966603, + 33.953417292456834, + 34.430438229418264, + 34.911345834551085, + 35.39614910352207, + 35.88485700094671, + 36.37747846067349, + 36.87402238606382, + 37.37449765026789, + 37.87891309649659, + 38.38727753828926, + 38.89959975977785, + 39.41588851594697, + 39.93615253289054, + 40.460400508064545, + 40.98864111053629, + 41.520882981230194, + 42.05713473317016, + 42.597404951718396, + 43.141702194811224, + 43.6900349931913, + 44.24241185063697, + 44.798841244188324, + 45.35933162437017, + 45.92389141541209, + 46.49252901546552, + 47.065252796817916, + 47.64207110610409, + 48.22299226451468, + 48.808024568002054, + 49.3971762874833, + 49.9904556690408, + 50.587870934119984, + 51.189430279724725, + 51.79514187861014, + 52.40501387947288, + 53.0190544071392, + 53.637271562750364, + 54.259673423945976, + 54.88626804504493, + 55.517063457223934, + 56.15206766869424, + 56.79128866487574, + 57.43473440856916, + 58.08241284012621, + 58.734331877617365, + 59.39049941699807, + 60.05092333227251, + 60.715611475655585, + 61.38457167773311, + 62.057811747619894, + 62.7353394731159, + 63.417162620860914, + 64.10328893648692, + 64.79372614476921, + 65.48848194977529, + 66.18756403501224, + 66.89098006357258, + 67.59873767827808, + 68.31084450182222, + 69.02730813691093, + 69.74813616640164, + 70.47333615344107, + 71.20291564160104, + 71.93688215501312, + 72.67524319850172, + 73.41800625771542, + 74.16517879925733, + 74.9167682708136, + 75.67278210128072, + 76.43322770089146, + 77.1981124613393, + 77.96744375590167, + 78.74122893956174, + 79.51947534912904, + 80.30219030335869, + 81.08938110306934, + 81.88105503125999, + 82.67721935322541, + 83.4778813166706, + 84.28304815182372, + 85.09272707154808, + 85.90692527145302, + 86.72564993000343, + 87.54890820862819, + 88.3767072518277, + 89.2090541872801, + 90.04595612594655, + 90.88742016217518, + 91.73345337380438, + 92.58406282226491, + 93.43925555268066, + 94.29903859396902, + 95.16341895893969, + 96.03240364439274, + 96.9059996312159, + 97.78421388448044, + 98.6670533535366, + 99.55452497210776 + ) + + fun sanitizeRadians(angle: Double): Double { + return (angle + kotlin.math.PI * 8) % (kotlin.math.PI * 2) + } + + fun trueDelinearized(rgbComponent: Double): Double { + val normalized = rgbComponent / 100.0 + var delinearized = 0.0 + delinearized = if (normalized <= 0.0031308) { + normalized * 12.92 + } else { + 1.055 * normalized.pow(1.0 / 2.4) - 0.055 + } + return delinearized * 255.0 + } + + fun chromaticAdaptation(component: Double): Double { + val af = abs(component).pow(0.42) + return MathUtils.signum(component) * 400.0 * af / (af + 27.13) + } + + fun hueOf(linrgb: DoubleArray): Double { + val scaledDiscount = MathUtils.matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB) + val rA = chromaticAdaptation(scaledDiscount[0]) + val gA = chromaticAdaptation(scaledDiscount[1]) + val bA = chromaticAdaptation(scaledDiscount[2]) + val a = (11.0 * rA + -12.0 * gA + bA) / 11.0 + val b = (rA + gA - 2.0 * bA) / 9.0 + return atan2(b, a) + } + + fun areInCyclicOrder(a: Double, b: Double, c: Double): Boolean { + val deltaAB = sanitizeRadians(b - a) + val deltaAC = sanitizeRadians(c - a) + return deltaAB < deltaAC + } + + fun intercept(source: Double, mid: Double, target: Double): Double { + return (mid - source) / (target - source) + } + + fun lerpPoint(source: DoubleArray, t: Double, target: DoubleArray): DoubleArray { + return doubleArrayOf( + source[0] + (target[0] - source[0]) * t, + source[1] + (target[1] - source[1]) * t, + source[2] + (target[2] - source[2]) * t + ) + } + + fun setCoordinate( + source: DoubleArray, + coordinate: Double, + target: DoubleArray, + axis: Int + ): DoubleArray { + val t = intercept(source[axis], coordinate, target[axis]) + return lerpPoint(source, t, target) + } + + fun isBounded(x: Double): Boolean { + return x in 0.0..100.0 + } + + fun nthVertex(y: Double, n: Int): DoubleArray { + val kR = Y_FROM_LINRGB[0] + val kG = Y_FROM_LINRGB[1] + val kB = Y_FROM_LINRGB[2] + val coordA = if (n % 4 <= 1) 0.0 else 100.0 + val coordB = if (n % 2 == 0) 0.0 else 100.0 + return if (n < 4) { + val r = (y - coordA * kG - coordB * kB) / kR + if (isBounded(r)) { + doubleArrayOf(r, coordA, coordB) + } else { + doubleArrayOf(-1.0, -1.0, -1.0) + } + } else if (n < 8) { + val g = (y - coordB * kR - coordA * kB) / kG + if (isBounded(g)) { + doubleArrayOf(coordB, g, coordA) + } else { + doubleArrayOf(-1.0, -1.0, -1.0) + } + } else { + val b = (y - coordA * kR - coordB * kG) / kB + if (isBounded(b)) { + doubleArrayOf(coordA, coordB, b) + } else { + doubleArrayOf(-1.0, -1.0, -1.0) + } + } + } + + fun bisectToSegment(y: Double, targetHue: Double): Array { + var left = doubleArrayOf(-1.0, -1.0, -1.0) + var right = left + var leftHue = 0.0 + var rightHue = 0.0 + var initialized = false + var uncut = true + for (n in 0..11) { + val mid = nthVertex(y, n) + if (mid[0] < 0) { + continue + } + val midHue = hueOf(mid) + if (!initialized) { + left = mid + right = mid + leftHue = midHue + rightHue = midHue + initialized = true + continue + } + if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) { + uncut = false + if (areInCyclicOrder(leftHue, targetHue, midHue)) { + right = mid + rightHue = midHue + } else { + left = mid + leftHue = midHue + } + } + } + return arrayOf(left, right) + } + + fun midpoint(a: DoubleArray, b: DoubleArray): DoubleArray { + return doubleArrayOf((a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2) + } + + fun criticalPlaneBelow(x: Double): Int { + return floor(x - 0.5).toInt() + } + + fun criticalPlaneAbove(x: Double): Int { + return ceil(x - 0.5).toInt() + } + + fun bisectToLimit(y: Double, targetHue: Double): DoubleArray { + val segment = bisectToSegment(y, targetHue) + var left = segment[0] + var leftHue = hueOf(left) + var right = segment[1] + for (axis in 0..2) { + if (left[axis] != right[axis]) { + var lPlane = -1 + var rPlane = 255 + if (left[axis] < right[axis]) { + lPlane = criticalPlaneBelow( + trueDelinearized( + left[axis] + ) + ) + rPlane = criticalPlaneAbove( + trueDelinearized( + right[axis] + ) + ) + } else { + lPlane = criticalPlaneAbove( + trueDelinearized( + left[axis] + ) + ) + rPlane = criticalPlaneBelow( + trueDelinearized( + right[axis] + ) + ) + } + for (i in 0..7) { + if (abs(rPlane - lPlane) <= 1) { + break + } else { + val mPlane = floor((lPlane + rPlane) / 2.0).toInt() + val midPlaneCoordinate = CRITICAL_PLANES[mPlane] + val mid = setCoordinate(left, midPlaneCoordinate, right, axis) + val midHue = hueOf(mid) + if (areInCyclicOrder(leftHue, targetHue, midHue)) { + right = mid + rPlane = mPlane + } else { + left = mid + leftHue = midHue + lPlane = mPlane + } + } + } + } + } + return midpoint(left, right) + } + + fun inverseChromaticAdaptation(adapted: Double): Double { + val adaptedAbs = abs(adapted) + val base = max(0.0, 27.13 * adaptedAbs / (400.0 - adaptedAbs)) + return MathUtils.signum(adapted) * base.pow(1.0 / 0.42) + } + + fun findResultByJ(hueRadians: Double, chroma: Double, y: Double): Int { + var j = sqrt(y) * 11.0 + val viewingConditions: ViewingConditions = ViewingConditions.DEFAULT + val tInnerCoeff = 1 / (1.64 - 0.29.pow(viewingConditions.n)).pow(0.73) + val eHue = 0.25 * (cos(hueRadians + 2.0) + 3.8) + val p1 = eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb + val hSin = sin(hueRadians) + val hCos = cos(hueRadians) + for (iterationRound in 0..4) { + val jNormalized = j / 100.0 + val alpha = if (chroma == 0.0 || j == 0.0) 0.0 else chroma / sqrt(jNormalized) + val t = (alpha * tInnerCoeff).pow(1.0 / 0.9) + val ac = (viewingConditions.aw * jNormalized.pow(1.0 / viewingConditions.c / viewingConditions.z)) + val p2 = ac / viewingConditions.nbb + val gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin) + val a = gamma * hCos + val b = gamma * hSin + val rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0 + val gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0 + val bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0 + val rCScaled = inverseChromaticAdaptation(rA) + val gCScaled = inverseChromaticAdaptation(gA) + val bCScaled = inverseChromaticAdaptation(bA) + val linrgb = MathUtils.matrixMultiply( + doubleArrayOf(rCScaled, gCScaled, bCScaled), + LINRGB_FROM_SCALED_DISCOUNT + ) + if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) { + return 0 + } + val kR = Y_FROM_LINRGB[0] + val kG = Y_FROM_LINRGB[1] + val kB = Y_FROM_LINRGB[2] + val fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2] + if (fnj <= 0) { + return 0 + } + if (iterationRound == 4 || abs(fnj - y) < 0.002) { + return if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) { + 0 + } else ColorUtils.argbFromLinrgb(linrgb) + } + j -= (fnj - y) * j / (2 * fnj) + } + return 0 + } + + fun solveToInt(hueDegrees: Double, chroma: Double, lstar: Double): Int { + var hueDegrees = hueDegrees + if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) { + return ColorUtils.argbFromLstar(lstar) + } + hueDegrees = MathUtils.sanitizeDegreesDouble(hueDegrees) + val hueRadians = hueDegrees / 180 * kotlin.math.PI + val y = ColorUtils.yFromLstar(lstar) + val exactAnswer = findResultByJ(hueRadians, chroma, y) + if (exactAnswer != 0) { + return exactAnswer + } + val linrgb = bisectToLimit(y, hueRadians) + return ColorUtils.argbFromLinrgb(linrgb) + } + + fun solveToCam(hueDegrees: Double, chroma: Double, lstar: Double): Cam16 { + return Cam16.Companion.fromInt(solveToInt(hueDegrees, chroma, lstar)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/ViewingConditions.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/ViewingConditions.kt new file mode 100644 index 000000000..c25ab809e --- /dev/null +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/hct/ViewingConditions.kt @@ -0,0 +1,93 @@ +package org.michaelbel.movies.ui.color.hct + +import kotlin.math.PI +import kotlin.math.exp +import kotlin.math.max +import kotlin.math.pow +import kotlin.math.sqrt +import org.michaelbel.movies.ui.color.utils.ColorUtils +import org.michaelbel.movies.ui.color.utils.MathUtils + +class ViewingConditions + +private constructor( + val n: Double, + val aw: Double, + val nbb: Double, + val ncb: Double, + val c: Double, + val nc: Double, + val rgbD: DoubleArray, + val fl: Double, + val flRoot: Double, + val z: Double +) { + + companion object { + val DEFAULT = defaultWithBackgroundLstar(50.0) + + fun make( + whitePoint: DoubleArray?, + adaptingLuminance: Double, + backgroundLstar: Double, + surround: Double, + discountingIlluminant: Boolean + ): ViewingConditions { + var backgroundLstar = backgroundLstar + backgroundLstar = max(0.1, backgroundLstar) + val matrix: Array = Cam16.XYZ_TO_CAM16RGB + val rW = whitePoint!![0] * matrix[0][0] + whitePoint[1] * matrix[0][1] + whitePoint[2] * matrix[0][2] + val gW = whitePoint[0] * matrix[1][0] + whitePoint[1] * matrix[1][1] + whitePoint[2] * matrix[1][2] + val bW = whitePoint[0] * matrix[2][0] + whitePoint[1] * matrix[2][1] + whitePoint[2] * matrix[2][2] + val f = 0.8 + surround / 10.0 + val c = if (f >= 0.9) MathUtils.lerp( + 0.59, + 0.69, + (f - 0.9) * 10.0 + ) else MathUtils.lerp(0.525, 0.59, (f - 0.8) * 10.0) + var d = if (discountingIlluminant) 1.0 else f * (1.0 - 1.0 / 3.6 * exp((-adaptingLuminance - 42.0) / 92.0)) + d = MathUtils.clampDouble(0.0, 1.0, d) + val rgbD = doubleArrayOf(d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d, d * (100.0 / bW) + 1.0 - d) + val k = 1.0 / (5.0 * adaptingLuminance + 1.0) + val k4 = k * k * k * k + val k4F = 1.0 - k4 + val fl = k4 * adaptingLuminance + 0.1 * k4F * k4F * kotlin.math.cbrt(5.0 * adaptingLuminance) + val n = ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1] + val z = 1.48 + sqrt(n) + val nbb = 0.725 / n.pow(0.2) + val rgbAFactors = doubleArrayOf( + (fl * rgbD[0] * rW / 100.0).pow(0.42), + (fl * rgbD[1] * gW / 100.0).pow(0.42), + (fl * rgbD[2] * bW / 100.0).pow(0.42) + ) + val rgbA = doubleArrayOf( + 400.0 * rgbAFactors[0] / (rgbAFactors[0] + 27.13), + 400.0 * rgbAFactors[1] / (rgbAFactors[1] + 27.13), + 400.0 * rgbAFactors[2] / (rgbAFactors[2] + 27.13) + ) + val aw = (2.0 * rgbA[0] + rgbA[1] + 0.05 * rgbA[2]) * nbb + return ViewingConditions( + n, + aw, + nbb, + nbb, + c, + f, + rgbD, + fl, + fl.pow(0.25), + z + ) + } + + fun defaultWithBackgroundLstar(lstar: Double): ViewingConditions { + return make( + ColorUtils.whitePointD65(), + 200.0 / PI * ColorUtils.yFromLstar(50.0) / 100f, + lstar, + 2.0, + false + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/ColorUtils.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/ColorUtils.kt new file mode 100644 index 000000000..82556ab6c --- /dev/null +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/ColorUtils.kt @@ -0,0 +1,166 @@ +package org.michaelbel.movies.ui.color.utils + +import kotlin.math.pow +import kotlin.math.roundToLong + +object ColorUtils { + val SRGB_TO_XYZ = arrayOf( + doubleArrayOf(0.41233895, 0.35762064, 0.18051042), + doubleArrayOf(0.2126, 0.7152, 0.0722), + doubleArrayOf(0.01932141, 0.11916382, 0.95034478) + ) + val XYZ_TO_SRGB = arrayOf( + doubleArrayOf(3.2413774792388685, -1.5376652402851851, -0.49885366846268053), + doubleArrayOf(-0.9691452513005321, 1.8758853451067872, 0.04156585616912061), + doubleArrayOf(0.05562093689691305, -0.20395524564742123, 1.0571799111220335) + ) + val WHITE_POINT_D65 = doubleArrayOf(95.047, 100.0, 108.883) + + fun argbFromRgb(red: Int, green: Int, blue: Int): Int { + return 255 shl 24 or (red and 255 shl 16) or (green and 255 shl 8) or (blue and 255) + } + + fun argbFromLinrgb(linrgb: DoubleArray?): Int { + val r = delinearized(linrgb!![0]) + val g = delinearized(linrgb[1]) + val b = delinearized(linrgb[2]) + return argbFromRgb(r, g, b) + } + + fun alphaFromArgb(argb: Int): Int { + return argb shr 24 and 255 + } + + fun redFromArgb(argb: Int): Int { + return argb shr 16 and 255 + } + + fun greenFromArgb(argb: Int): Int { + return argb shr 8 and 255 + } + + fun blueFromArgb(argb: Int): Int { + return argb and 255 + } + + fun isOpaque(argb: Int): Boolean { + return alphaFromArgb(argb) >= 255 + } + + fun argbFromXyz(x: Double, y: Double, z: Double): Int { + val matrix = XYZ_TO_SRGB + val linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z + val linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z + val linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z + val r = delinearized(linearR) + val g = delinearized(linearG) + val b = delinearized(linearB) + return argbFromRgb(r, g, b) + } + + fun xyzFromArgb(argb: Int): DoubleArray? { + val r = linearized(redFromArgb(argb)) + val g = linearized(greenFromArgb(argb)) + val b = linearized(blueFromArgb(argb)) + return MathUtils.matrixMultiply(doubleArrayOf(r, g, b), SRGB_TO_XYZ) + } + + fun argbFromLab(l: Double, a: Double, b: Double): Int { + val whitePoint = WHITE_POINT_D65 + val fy = (l + 16.0) / 116.0 + val fx = a / 500.0 + fy + val fz = fy - b / 200.0 + val xNormalized = labInvf(fx) + val yNormalized = labInvf(fy) + val zNormalized = labInvf(fz) + val x = xNormalized * whitePoint[0] + val y = yNormalized * whitePoint[1] + val z = zNormalized * whitePoint[2] + return argbFromXyz(x, y, z) + } + + fun labFromArgb(argb: Int): DoubleArray { + val linearR = linearized(redFromArgb(argb)) + val linearG = linearized(greenFromArgb(argb)) + val linearB = linearized(blueFromArgb(argb)) + val matrix = SRGB_TO_XYZ + val x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB + val y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB + val z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB + val whitePoint = WHITE_POINT_D65 + val xNormalized = x / whitePoint[0] + val yNormalized = y / whitePoint[1] + val zNormalized = z / whitePoint[2] + val fx = labF(xNormalized) + val fy = labF(yNormalized) + val fz = labF(zNormalized) + val l = 116.0 * fy - 16 + val a = 500.0 * (fx - fy) + val b = 200.0 * (fy - fz) + return doubleArrayOf(l, a, b) + } + + fun argbFromLstar(lstar: Double): Int { + val y = yFromLstar(lstar) + val component = delinearized(y) + return argbFromRgb(component, component, component) + } + + fun lstarFromArgb(argb: Int): Double { + val y = xyzFromArgb(argb)!![1] + return 116.0 * labF(y / 100.0) - 16.0 + } + + fun yFromLstar(lstar: Double): Double { + return 100.0 * labInvf((lstar + 16.0) / 116.0) + } + + fun lstarFromY(y: Double): Double { + return labF(y / 100.0) * 116.0 - 16.0 + } + + fun linearized(rgbComponent: Int): Double { + val normalized = rgbComponent / 255.0 + return if (normalized <= 0.040449936) { + normalized / 12.92 * 100.0 + } else { + ((normalized + 0.055) / 1.055).pow(2.4) * 100.0 + } + } + + fun delinearized(rgbComponent: Double): Int { + val normalized = rgbComponent / 100.0 + var delinearized = 0.0 + delinearized = if (normalized <= 0.0031308) { + normalized * 12.92 + } else { + 1.055 * normalized.pow(1.0 / 2.4) - 0.055 + } + return MathUtils.clampInt(0, 255, (delinearized * 255.0).roundToLong().toInt()) + } + + fun whitePointD65(): DoubleArray { + return WHITE_POINT_D65 + } + + fun labF(t: Double): Double { + val e = 216.0 / 24389.0 + val kappa = 24389.0 / 27.0 + return if (t > e) { + t.pow(1.0 / 3.0) + } else { + (kappa * t + 16) / 116 + } + } + + fun labInvf(ft: Double): Double { + val e = 216.0 / 24389.0 + val kappa = 24389.0 / 27.0 + val ft3 = ft * ft * ft + return if (ft3 > e) { + ft3 + } else { + (116 * ft - 16) / kappa + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/MathUtils.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/MathUtils.kt new file mode 100644 index 000000000..a67fcd332 --- /dev/null +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/MathUtils.kt @@ -0,0 +1,72 @@ +package org.michaelbel.movies.ui.color.utils + +import kotlin.math.abs + +object MathUtils { + + fun signum(num: Double): Int { + return if (num < 0) { + -1 + } else if (num == 0.0) { + 0 + } else { + 1 + } + } + + fun lerp(start: Double, stop: Double, amount: Double): Double { + return (1.0 - amount) * start + amount * stop + } + + fun clampInt(min: Int, max: Int, input: Int): Int { + if (input < min) { + return min + } else if (input > max) { + return max + } + return input + } + + fun clampDouble(min: Double, max: Double, input: Double): Double { + if (input < min) { + return min + } else if (input > max) { + return max + } + return input + } + + fun sanitizeDegreesInt(degrees: Int): Int { + var degrees = degrees + degrees %= 360 + if (degrees < 0) { + degrees += 360 + } + return degrees + } + + fun sanitizeDegreesDouble(degrees: Double): Double { + var degrees = degrees + degrees %= 360.0 + if (degrees < 0) { + degrees += 360.0 + } + return degrees + } + + fun rotationDirection(from: Double, to: Double): Double { + val increasingDifference = sanitizeDegreesDouble(to - from) + return if (increasingDifference <= 180.0) 1.0 else -1.0 + } + + fun differenceDegrees(a: Double, b: Double): Double { + return 180.0 - abs(abs(a - b) - 180.0) + } + + fun matrixMultiply(row: DoubleArray, matrix: Array): DoubleArray { + val a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2] + val b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2] + val c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2] + return doubleArrayOf(a, b, c) + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/StringUtils.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/StringUtils.kt new file mode 100644 index 000000000..34f0f164c --- /dev/null +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/color/utils/StringUtils.kt @@ -0,0 +1,10 @@ +package org.michaelbel.movies.ui.color.utils + +internal object StringUtils { + fun hexFromArgb(argb: Int): String { + val red = ColorUtils.redFromArgb(argb) + val blue = ColorUtils.blueFromArgb(argb) + val green = ColorUtils.greenFromArgb(argb) + return String.format("#%02x%02x%02x", red, green, blue) + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt index 8adf68cdc..eed0510fc 100644 --- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt @@ -17,12 +17,14 @@ import androidx.compose.material.icons.outlined.Language import androidx.compose.material.icons.outlined.LocalMovies import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.outlined.SystemUpdate import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.Widgets import androidx.compose.ui.graphics.vector.ImageVector import org.michaelbel.movies.ui.R @@ -36,6 +38,8 @@ object MoviesIcons { @DrawableRes val FileDownload24 = R.drawable.ic_file_download_24 @DrawableRes val AdultOutline = R.drawable.ic_18_up_rating_outline_24 @DrawableRes val Cat = R.drawable.ic_cat_24 + @DrawableRes val Github = R.drawable.ic_github_24 + @DrawableRes val GooglePlay = R.drawable.ic_google_play_24 val AccountCircle = Icons.Outlined.AccountCircle val ArrowBack = Icons.AutoMirrored.Outlined.ArrowBack @@ -52,10 +56,12 @@ object MoviesIcons { val MovieFilter = Icons.Filled.MovieFilter val LocalMovies = Icons.Outlined.LocalMovies val Notifications = Icons.Outlined.Notifications + val Palette = Icons.Outlined.Palette val Search = Icons.Outlined.Search val Settings = Icons.Outlined.Settings val Share = Icons.Outlined.Share val SystemUpdate = Icons.Outlined.SystemUpdate val Visibility = Icons.Outlined.Visibility val VisibilityOff = Icons.Outlined.VisibilityOff + val Widgets = Icons.Outlined.Widgets } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_github_24.xml b/core/ui/src/main/res/drawable/ic_github_24.xml new file mode 100644 index 000000000..3be173dd3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_github_24.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_google_play_24.xml b/core/ui/src/main/res/drawable/ic_google_play_24.xml new file mode 100644 index 000000000..4b337e7dc --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_google_play_24.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt index f0c8be39a..a89b4a615 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt @@ -32,6 +32,8 @@ class SettingsViewModel @Inject constructor( val isPostNotificationsFeatureEnabled = Build.VERSION.SDK_INT >= 33 + val isReviewFeatureEnabled = interactor.isReviewFeatureEnabled + val currentTheme: StateFlow = interactor.currentTheme .stateIn( scope = this, diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt deleted file mode 100644 index 3bd647685..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.michaelbel.movies.settings.ktx - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import org.michaelbel.movies.common.localization.model.AppLanguage -import org.michaelbel.movies.settings_impl.R - -internal val AppLanguage.languageText: String - @Composable get() = when (this) { - is AppLanguage.English -> stringResource(R.string.settings_language_en) - is AppLanguage.Russian -> stringResource(R.string.settings_language_ru) - } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/FeedViewKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/FeedViewKtx.kt deleted file mode 100644 index 4c17fed6c..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/FeedViewKtx.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.michaelbel.movies.settings.ktx - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import org.michaelbel.movies.common.appearance.FeedView -import org.michaelbel.movies.settings_impl.R - -internal val FeedView.feedViewText: String - @Composable get() = when (this) { - is FeedView.FeedList -> stringResource(R.string.settings_appearance_list) - is FeedView.FeedGrid -> stringResource(R.string.settings_appearance_grid) - } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/GrammaticalGenderKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/GrammaticalGenderKtx.kt deleted file mode 100644 index e4971be74..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/GrammaticalGenderKtx.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.michaelbel.movies.settings.ktx - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import org.michaelbel.movies.common.gender.GrammaticalGender -import org.michaelbel.movies.settings_impl.R - -internal val GrammaticalGender.genderText: String - @Composable get() = when (this) { - is GrammaticalGender.NotSpecified -> stringResource(R.string.settings_gender_not_specified) - is GrammaticalGender.Neutral -> stringResource(R.string.settings_gender_neutral) - is GrammaticalGender.Feminine -> stringResource(R.string.settings_gender_feminine) - is GrammaticalGender.Masculine -> stringResource(R.string.settings_gender_masculine) - } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/IconAliasKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/IconAliasKtx.kt index 2b5848f87..a31e406ad 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/IconAliasKtx.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/IconAliasKtx.kt @@ -1,21 +1,8 @@ package org.michaelbel.movies.settings.ktx import android.content.Context -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import org.michaelbel.movies.settings_impl.R import org.michaelbel.movies.ui.appicon.IconAlias -import org.michaelbel.movies.ui.appicon.isEnabled - -internal val IconAlias.iconText: String - @Composable get() = when (this) { - is IconAlias.Red -> stringResource(R.string.settings_app_launcher_icon_red) - is IconAlias.Purple -> stringResource(R.string.settings_app_launcher_icon_purple) - is IconAlias.Brown -> stringResource(R.string.settings_app_launcher_icon_brown) - is IconAlias.Amoled -> stringResource(R.string.settings_app_launcher_icon_amoled) - } internal fun IconAlias.iconSnackbarText(context: Context): String { return when (this) { @@ -24,12 +11,4 @@ internal fun IconAlias.iconSnackbarText(context: Context): String { is IconAlias.Brown -> context.getString(R.string.settings_app_launcher_icon_brown) is IconAlias.Amoled -> context.getString(R.string.settings_app_launcher_icon_amoled) } -} - -@Composable -internal fun IconAlias.backgroundColor(context: Context): Color { - return when { - context.isEnabled(this) -> MaterialTheme.colorScheme.inversePrimary - else -> MaterialTheme.colorScheme.primaryContainer - } } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/MovieListKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/MovieListKtx.kt deleted file mode 100644 index 9848b8a8c..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/MovieListKtx.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.michaelbel.movies.settings.ktx - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import org.michaelbel.movies.common.list.MovieList -import org.michaelbel.movies.settings_impl.R - -internal val MovieList.listText: String - @Composable get() = when (this) { - is MovieList.NowPlaying -> stringResource(R.string.settings_movie_list_now_playing) - is MovieList.Popular -> stringResource(R.string.settings_movie_list_popular) - is MovieList.TopRated -> stringResource(R.string.settings_movie_list_top_rated) - is MovieList.Upcoming -> stringResource(R.string.settings_movie_list_upcoming) - } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SealedStringKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SealedStringKtx.kt new file mode 100644 index 000000000..543c29196 --- /dev/null +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SealedStringKtx.kt @@ -0,0 +1,37 @@ +package org.michaelbel.movies.settings.ktx + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import org.michaelbel.movies.common.SealedString +import org.michaelbel.movies.common.appearance.FeedView +import org.michaelbel.movies.common.gender.GrammaticalGender +import org.michaelbel.movies.common.list.MovieList +import org.michaelbel.movies.common.localization.model.AppLanguage +import org.michaelbel.movies.common.theme.AppTheme +import org.michaelbel.movies.settings_impl.R + +internal val SealedString.stringText: String + @Composable get() = when (this) { + is AppLanguage.English -> stringResource(R.string.settings_language_en) + is AppLanguage.Russian -> stringResource(R.string.settings_language_ru) + + is AppTheme.NightNo -> stringResource(R.string.settings_theme_light) + is AppTheme.NightYes -> stringResource(R.string.settings_theme_dark) + is AppTheme.FollowSystem -> stringResource(R.string.settings_theme_system) + is AppTheme.Amoled -> stringResource(R.string.settings_theme_amoled) + + is FeedView.FeedList -> stringResource(R.string.settings_appearance_list) + is FeedView.FeedGrid -> stringResource(R.string.settings_appearance_grid) + + is GrammaticalGender.NotSpecified -> stringResource(R.string.settings_gender_not_specified) + is GrammaticalGender.Neutral -> stringResource(R.string.settings_gender_neutral) + is GrammaticalGender.Feminine -> stringResource(R.string.settings_gender_feminine) + is GrammaticalGender.Masculine -> stringResource(R.string.settings_gender_masculine) + + is MovieList.NowPlaying -> stringResource(R.string.settings_movie_list_now_playing) + is MovieList.Popular -> stringResource(R.string.settings_movie_list_popular) + is MovieList.TopRated -> stringResource(R.string.settings_movie_list_top_rated) + is MovieList.Upcoming -> stringResource(R.string.settings_movie_list_upcoming) + + else -> "" + } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt deleted file mode 100644 index e182e3438..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.michaelbel.movies.settings.ktx - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings_impl.R - -internal val AppTheme.themeText: String - @Composable get() = when (this) { - is AppTheme.NightNo -> stringResource(R.string.settings_theme_light) - is AppTheme.NightYes -> stringResource(R.string.settings_theme_dark) - is AppTheme.FollowSystem -> stringResource(R.string.settings_theme_system) - is AppTheme.Amoled -> stringResource(R.string.settings_theme_amoled) - } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/AppIconBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/AppIconBox.kt deleted file mode 100644 index 2d6d05a1b..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/AppIconBox.kt +++ /dev/null @@ -1,120 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ChainStyle -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.backgroundColor -import org.michaelbel.movies.settings.ktx.iconText -import org.michaelbel.movies.ui.accessibility.MoviesContentDescription -import org.michaelbel.movies.ui.appicon.IconAlias -import org.michaelbel.movies.ui.appicon.isEnabled -import org.michaelbel.movies.ui.ktx.context -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.IconAliasPreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun AppIconBox( - iconAlias: IconAlias, - modifier: Modifier = Modifier -) { - ConstraintLayout( - modifier = modifier - ) { - val (icon, radio, text) = createRefs() - createHorizontalChain(radio, text, chainStyle = ChainStyle.Packed) - - Icon( - painter = painterResource(iconAlias.iconRes), - contentDescription = stringResource(MoviesContentDescription.AppIcon), - modifier = Modifier - .constrainAs(icon) { - width = Dimension.value(56.dp) - height = Dimension.value(56.dp) - start.linkTo(parent.start, 8.dp) - top.linkTo(parent.top, 8.dp) - end.linkTo(parent.end, 8.dp) - } - .clip(CircleShape), - tint = Color.Unspecified - ) - - RadioButton( - selected = context.isEnabled(iconAlias), - onClick = null, - modifier = Modifier.constrainAs(radio) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start) - top.linkTo(icon.bottom, 8.dp) - end.linkTo(text.start) - bottom.linkTo(parent.bottom, 8.dp) - } - ) - - Text( - text = iconAlias.iconText, - modifier = Modifier - .constrainAs(text) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(radio.end) - top.linkTo(radio.top) - end.linkTo(parent.end) - bottom.linkTo(radio.bottom) - } - .padding(start = 2.dp), - style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } -} - -@Composable -@DevicePreviews -private fun AppIconBoxPreview( - @PreviewParameter(IconAliasPreviewParameterProvider::class) iconAlias: IconAlias -) { - MoviesTheme { - AppIconBox( - iconAlias = iconAlias, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(iconAlias.backgroundColor(context)) - ) - } -} - -@Composable -@Preview -private fun AppIconBoxAmoledPreview( - @PreviewParameter(IconAliasPreviewParameterProvider::class) iconAlias: IconAlias -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - AppIconBox( - iconAlias = iconAlias, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(iconAlias.backgroundColor(context)) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppIconBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppIconBox.kt deleted file mode 100644 index 7a01855ec..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppIconBox.kt +++ /dev/null @@ -1,146 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.backgroundColor -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.appicon.IconAlias -import org.michaelbel.movies.ui.appicon.setIcon -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsAppIconBox( - onAppIconChanged: (IconAlias) -> Unit, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - - fun changeAppIcon(iconAlias: IconAlias) { - onAppIconChanged(iconAlias) - context.setIcon(iconAlias) - } - - ConstraintLayout( - modifier = modifier - ) { - val (title, redBox, purpleBox, brownBox, amoledBox) = createRefs() - - Text( - text = stringResource(R.string.settings_app_launcher_icon), - modifier = Modifier.constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top, 8.dp) - }, - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - AppIconBox( - iconAlias = IconAlias.Red, - modifier = Modifier - .constrainAs(redBox) { - height = Dimension.wrapContent - start.linkTo(parent.start, 8.dp) - top.linkTo(title.bottom, 8.dp) - end.linkTo(purpleBox.start, 4.dp) - bottom.linkTo(brownBox.top, 8.dp) - } - .fillMaxWidth(.45F) - .clip(RoundedCornerShape(8.dp)) - .background(IconAlias.Red.backgroundColor(context)) - .clickable { changeAppIcon(IconAlias.Red) } - ) - - AppIconBox( - iconAlias = IconAlias.Purple, - modifier = Modifier - .constrainAs(purpleBox) { - height = Dimension.wrapContent - start.linkTo(redBox.end, 4.dp) - top.linkTo(title.bottom, 8.dp) - end.linkTo(parent.end, 8.dp) - bottom.linkTo(amoledBox.top, 8.dp) - } - .fillMaxWidth(.45F) - .clip(RoundedCornerShape(8.dp)) - .background(IconAlias.Purple.backgroundColor(context)) - .clickable { changeAppIcon(IconAlias.Purple) } - ) - - AppIconBox( - iconAlias = IconAlias.Brown, - modifier = Modifier - .constrainAs(brownBox) { - height = Dimension.wrapContent - start.linkTo(parent.start, 8.dp) - top.linkTo(redBox.bottom, 8.dp) - end.linkTo(amoledBox.start, 4.dp) - bottom.linkTo(parent.bottom, 16.dp) - } - .fillMaxWidth(.45F) - .clip(RoundedCornerShape(8.dp)) - .background(IconAlias.Brown.backgroundColor(context)) - .clickable { changeAppIcon(IconAlias.Brown) } - ) - - AppIconBox( - iconAlias = IconAlias.Amoled, - modifier = Modifier - .constrainAs(amoledBox) { - height = Dimension.wrapContent - start.linkTo(brownBox.end, 4.dp) - top.linkTo(purpleBox.bottom, 8.dp) - end.linkTo(parent.end, 8.dp) - bottom.linkTo(parent.bottom, 16.dp) - } - .fillMaxWidth(.45F) - .clip(RoundedCornerShape(8.dp)) - .background(IconAlias.Amoled.backgroundColor(context)) - .clickable { changeAppIcon(IconAlias.Amoled) } - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsAppIconBoxPreview() { - MoviesTheme { - SettingsAppIconBox( - onAppIconChanged = {}, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsAppIconBoxAmoledPreview() { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsAppIconBox( - onAppIconChanged = {}, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppWidgetBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppWidgetBox.kt deleted file mode 100644 index 6e361ef74..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppWidgetBox.kt +++ /dev/null @@ -1,87 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import android.appwidget.AppWidgetManager -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.theme.MoviesTheme -import org.michaelbel.movies.widget.ktx.pin - -@Composable -internal fun SettingsAppWidgetBox( - modifier: Modifier = Modifier -) { - val context = LocalContext.current - val appWidgetManager by remember { mutableStateOf(AppWidgetManager.getInstance(context)) } - val appWidgetProvider by remember { mutableStateOf(appWidgetManager.getInstalledProvidersForPackage(context.packageName, null).first()) } - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { appWidgetProvider.pin(context) } - .testTag("ConstraintLayout") - ) { - val (title) = createRefs() - - Text( - text = stringResource(R.string.settings_app_widget), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("Text"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsAppWidgetBoxPreview() { - MoviesTheme { - SettingsAppWidgetBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsAppWidgetBoxAmoledPreview() { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsAppWidgetBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceBox.kt deleted file mode 100644 index 6eb74d611..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceBox.kt +++ /dev/null @@ -1,123 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.appearance.FeedView -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.feedViewText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.AppearancePreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsAppearanceBox( - currentFeedView: FeedView, - onFeedViewSelect: (FeedView) -> Unit, - modifier: Modifier = Modifier, -) { - var feedViewDialog by remember { mutableStateOf(false) } - - if (feedViewDialog) { - SettingsAppearanceDialog( - currentFeedView = currentFeedView, - onFeedViewSelect = onFeedViewSelect, - onDismissRequest = { - feedViewDialog = false - } - ) - } - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { - feedViewDialog = true - } - .testTag("ConstraintLayout") - ) { - val (title, value) = createRefs() - - Text( - text = stringResource(R.string.settings_appearance), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("TitleText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - Text( - text = currentFeedView.feedViewText, - modifier = Modifier - .constrainAs(value) { - width = Dimension.wrapContent - height = Dimension.wrapContent - top.linkTo(parent.top) - end.linkTo(parent.end, 16.dp) - bottom.linkTo(parent.bottom) - } - .testTag("ValueText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.primary) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsAppearanceBoxPreview( - @PreviewParameter(AppearancePreviewParameterProvider::class) feedView: FeedView -) { - MoviesTheme { - SettingsAppearanceBox( - currentFeedView = feedView, - onFeedViewSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsAppearanceBoxAmoledPreview( - @PreviewParameter(AppearancePreviewParameterProvider::class) feedView: FeedView -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsAppearanceBox( - currentFeedView = feedView, - onFeedViewSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt deleted file mode 100644 index d92f652d1..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt +++ /dev/null @@ -1,157 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.RadioButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import org.michaelbel.movies.common.appearance.FeedView -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.feedViewText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.accessibility.MoviesContentDescription -import org.michaelbel.movies.ui.icons.MoviesIcons -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.AppearancePreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsAppearanceDialog( - currentFeedView: FeedView, - onFeedViewSelect: (FeedView) -> Unit, - onDismissRequest: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = onDismissRequest, - modifier = Modifier.testTag("ConfirmTextButton") - ) { - Text( - text = stringResource(R.string.settings_action_cancel), - modifier = Modifier.testTag("ConfirmText"), - style = MaterialTheme.typography.labelLarge.copy(MaterialTheme.colorScheme.primary) - ) - } - }, - icon = { - Icon( - imageVector = MoviesIcons.GridView, - contentDescription = stringResource(MoviesContentDescription.AppearanceIcon), - modifier = Modifier.testTag("Icon") - ) - }, - title = { - Text( - text = stringResource(R.string.settings_appearance), - modifier = Modifier.testTag("Title"), - style = MaterialTheme.typography.headlineSmall.copy(MaterialTheme.colorScheme.onSurface) - ) - }, - text = { - SettingAppearanceDialogContent( - currentFeedView = currentFeedView, - onFeedViewSelect = { feedView -> - onFeedViewSelect(feedView) - onDismissRequest() - }, - modifier = Modifier.testTag("Content") - ) - }, - shape = RoundedCornerShape(28.dp), - containerColor = MaterialTheme.colorScheme.surface, - iconContentColor = MaterialTheme.colorScheme.secondary, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -private fun SettingAppearanceDialogContent( - currentFeedView: FeedView, - onFeedViewSelect: (FeedView) -> Unit, - modifier: Modifier = Modifier -) { - val scrollState = rememberScrollState() - - Column( - modifier = modifier.verticalScroll(scrollState) - ) { - FeedView.VALUES.forEach { feedView -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .clickable { onFeedViewSelect(feedView) }, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = currentFeedView == feedView, - onClick = null, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6F) - ), - modifier = Modifier.padding(start = 16.dp) - ) - - Text( - text = feedView.feedViewText, - modifier = Modifier.padding(start = 8.dp), - style = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - ) - } - } - } -} - -@Composable -@DevicePreviews -private fun SettingsAppearanceDialogPreview( - @PreviewParameter(AppearancePreviewParameterProvider::class) feeView: FeedView -) { - MoviesTheme { - SettingsAppearanceDialog( - currentFeedView = feeView, - onFeedViewSelect = {}, - onDismissRequest = {} - ) - } -} - -@Composable -@Preview -private fun SettingsAppearanceDialogAmoledPreview( - @PreviewParameter(AppearancePreviewParameterProvider::class) feeView: FeedView -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsAppearanceDialog( - currentFeedView = feeView, - onFeedViewSelect = {}, - onDismissRequest = {} - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsDynamicColorsBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsDynamicColorsBox.kt deleted file mode 100644 index f85ad3c2b..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsDynamicColorsBox.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.SwitchDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.compose.SwitchCheckIcon -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.BooleanPreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsDynamicColorsBox( - isDynamicColorsEnabled: Boolean, - modifier: Modifier = Modifier -) { - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .testTag("ConstraintLayout") - ) { - val (title, value) = createRefs() - - Text( - text = stringResource(R.string.settings_dynamic_colors), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("Text"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - Switch( - checked = isDynamicColorsEnabled, - onCheckedChange = null, - modifier = Modifier - .constrainAs(value) { - width = Dimension.wrapContent - height = Dimension.wrapContent - top.linkTo(parent.top) - end.linkTo(parent.end, 16.dp) - bottom.linkTo(parent.bottom) - } - .testTag("Switch"), - thumbContent = if (isDynamicColorsEnabled) { { SwitchCheckIcon() } } else null, - colors = SwitchDefaults.colors( - checkedTrackColor = MaterialTheme.colorScheme.surfaceTint, - checkedIconColor = MaterialTheme.colorScheme.surfaceTint - ) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsDynamicColorsBoxPreview( - @PreviewParameter(BooleanPreviewParameterProvider::class) isEnabled: Boolean -) { - MoviesTheme { - SettingsDynamicColorsBox( - isDynamicColorsEnabled = isEnabled, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsDynamicColorsBoxAmoledPreview( - @PreviewParameter(BooleanPreviewParameterProvider::class) isEnabled: Boolean -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsDynamicColorsBox( - isDynamicColorsEnabled = isEnabled, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderBox.kt deleted file mode 100644 index 3706e33da..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderBox.kt +++ /dev/null @@ -1,116 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import android.app.GrammaticalInflectionManager -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.gender.GrammaticalGender -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.genderText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsGenderBox( - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - val grammaticalInflectionManager by remember { mutableStateOf(context.getSystemService(GrammaticalInflectionManager::class.java)) } - val grammaticalGender by remember { mutableStateOf(grammaticalInflectionManager.applicationGrammaticalGender) } - val currentGrammaticalGender by remember { mutableStateOf(GrammaticalGender.transform(grammaticalGender)) } - - var genderDialog by remember { mutableStateOf(false) } - - if (genderDialog) { - SettingsGenderDialog( - onDismissRequest = { - genderDialog = false - } - ) - } - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { - genderDialog = true - } - .testTag("ConstraintLayout") - ) { - val (title, value) = createRefs() - - Text( - text = stringResource(R.string.settings_gender), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("TitleText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - Text( - text = currentGrammaticalGender.genderText, - modifier = Modifier - .constrainAs(value) { - width = Dimension.wrapContent - height = Dimension.wrapContent - top.linkTo(parent.top) - end.linkTo(parent.end, 16.dp) - bottom.linkTo(parent.bottom) - } - .testTag("ValueText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.primary) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsGenderBoxPreview() { - MoviesTheme { - SettingsGenderBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsGenderBoxAmoledPreview() { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsGenderBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderDialog.kt deleted file mode 100644 index fa2e81581..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGenderDialog.kt +++ /dev/null @@ -1,153 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import android.app.GrammaticalInflectionManager -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.RadioButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import org.michaelbel.movies.common.gender.GrammaticalGender -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.genderText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.accessibility.MoviesContentDescription -import org.michaelbel.movies.ui.icons.MoviesIcons -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsGenderDialog( - onDismissRequest: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = onDismissRequest, - modifier = Modifier.testTag("ConfirmTextButton") - ) { - Text( - text = stringResource(R.string.settings_action_cancel), - modifier = Modifier.testTag("ConfirmText"), - style = MaterialTheme.typography.labelLarge.copy(MaterialTheme.colorScheme.primary) - ) - } - }, - icon = { - Icon( - painter = painterResource(MoviesIcons.Cat), - contentDescription = stringResource(MoviesContentDescription.AppearanceIcon), - modifier = Modifier.testTag("Icon") - ) - }, - title = { - Text( - text = stringResource(R.string.settings_gender), - modifier = Modifier.testTag("Title"), - style = MaterialTheme.typography.headlineSmall.copy(MaterialTheme.colorScheme.onSurface) - ) - }, - text = { - SettingsGenderDialogContent( - onDialogDismiss = { onDismissRequest() }, - modifier = Modifier.testTag("Content") - ) - }, - shape = RoundedCornerShape(28.dp), - containerColor = MaterialTheme.colorScheme.surface, - iconContentColor = MaterialTheme.colorScheme.secondary, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -private fun SettingsGenderDialogContent( - onDialogDismiss: () -> Unit, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - val grammaticalInflectionManager by remember { mutableStateOf(context.getSystemService(GrammaticalInflectionManager::class.java)) } - val grammaticalGender by remember { mutableStateOf(grammaticalInflectionManager.applicationGrammaticalGender) } - val currentGrammaticalGender by remember { mutableStateOf(GrammaticalGender.transform(grammaticalGender)) } - val scrollState = rememberScrollState() - - Column( - modifier = modifier.verticalScroll(scrollState) - ) { - GrammaticalGender.VALUES.forEach { gender -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .clickable { - grammaticalInflectionManager.setRequestedApplicationGrammaticalGender(gender.value) - onDialogDismiss() - }, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = currentGrammaticalGender == gender, - onClick = null, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6F) - ), - modifier = Modifier.padding(start = 16.dp) - ) - - Text( - text = gender.genderText, - modifier = Modifier.padding(start = 8.dp), - style = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - ) - } - } - } -} - -@Composable -@DevicePreviews -private fun SettingsGenderDialogPreview() { - MoviesTheme { - SettingsGenderDialog( - onDismissRequest = {} - ) - } -} - -@Composable -@Preview -private fun SettingsGenderDialogAmoledPreview() { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsGenderDialog( - onDismissRequest = {} - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGithubBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGithubBox.kt deleted file mode 100644 index dbe981538..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsGithubBox.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.MOVIES_GITHUB_URL -import org.michaelbel.movies.common.browser.openUrl -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsGithubBox( - modifier: Modifier = Modifier -) { - val resultContract = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {} - val toolbarColor = MaterialTheme.colorScheme.primary.toArgb() - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { openUrl(resultContract, toolbarColor, MOVIES_GITHUB_URL) } - .testTag("ConstraintLayout") - ) { - val (title) = createRefs() - - Text( - text = stringResource(R.string.settings_github), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("Text"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsGithubBoxPreview() { - MoviesTheme { - SettingsGithubBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsGithubBoxAmoledPreview() { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsGithubBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt deleted file mode 100644 index 85231e2c8..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt +++ /dev/null @@ -1,159 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.localization.model.AppLanguage -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.languageText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.accessibility.MoviesContentDescription -import org.michaelbel.movies.ui.icons.MoviesIcons -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.LanguagePreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsLanguageBox( - currentLanguage: AppLanguage, - onLanguageSelect: (AppLanguage) -> Unit, - modifier: Modifier = Modifier, -) { - var languageDropdown by remember { mutableStateOf(false) } - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { languageDropdown = true } - .testTag("ConstraintLayout") - ) { - val (icon, title, value) = createRefs() - - Icon( - imageVector = MoviesIcons.Language, - contentDescription = stringResource(MoviesContentDescription.LanguageIcon), - modifier = Modifier - .constrainAs(icon) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("Icon"), - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - - Text( - text = stringResource(R.string.settings_language), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(icon.end, 8.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("TitleText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - Box( - modifier = Modifier - .constrainAs(value) { - width = Dimension.wrapContent - height = Dimension.wrapContent - top.linkTo(parent.top) - end.linkTo(parent.end, 16.dp) - bottom.linkTo(parent.bottom) - } - .testTag("ValueText"), - ) { - Text( - text = currentLanguage.languageText, - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.primary) - ) - - DropdownMenu( - expanded = languageDropdown, - onDismissRequest = { languageDropdown = false }, - offset = DpOffset(x = 0.dp, y = (-48).dp), - modifier = Modifier.widthIn(min = 112.dp, max = 280.dp) - ) { - AppLanguage.VALUES.forEach { appLanguage -> - DropdownMenuItem( - text = { - Text( - text = appLanguage.languageText, - style = MaterialTheme.typography.bodyLarge - ) - }, - onClick = { - onLanguageSelect(appLanguage) - languageDropdown = false - } - ) - } - } - } - } -} - -@Composable -@DevicePreviews -private fun SettingsLanguageBoxPreview( - @PreviewParameter(LanguagePreviewParameterProvider::class) language: AppLanguage -) { - MoviesTheme { - SettingsLanguageBox( - currentLanguage = language, - onLanguageSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsLanguageBoxAmoledPreview( - @PreviewParameter(LanguagePreviewParameterProvider::class) language: AppLanguage -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsLanguageBox( - currentLanguage = language, - onLanguageSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListBox.kt deleted file mode 100644 index 162bcabc7..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListBox.kt +++ /dev/null @@ -1,123 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.list.MovieList -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.listText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.MovieListPreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsMovieListBox( - currentMovieList: MovieList, - onMovieListSelect: (MovieList) -> Unit, - modifier: Modifier = Modifier, -) { - var movieListDialog by remember { mutableStateOf(false) } - - if (movieListDialog) { - SettingsMovieListDialog( - currentMovieList = currentMovieList, - onMovieListSelect = onMovieListSelect, - onDismissRequest = { - movieListDialog = false - } - ) - } - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { - movieListDialog = true - } - .testTag("ConstraintLayout") - ) { - val (title, value) = createRefs() - - Text( - text = stringResource(R.string.settings_movie_list), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("TitleText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - Text( - text = currentMovieList.listText, - modifier = Modifier - .constrainAs(value) { - width = Dimension.wrapContent - height = Dimension.wrapContent - top.linkTo(parent.top) - end.linkTo(parent.end, 16.dp) - bottom.linkTo(parent.bottom) - } - .testTag("ValueText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.primary) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsMovieListBoxPreview( - @PreviewParameter(MovieListPreviewParameterProvider::class) movieList: MovieList -) { - MoviesTheme { - SettingsMovieListBox( - currentMovieList = movieList, - onMovieListSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsMovieListBoxAmoledPreview( - @PreviewParameter(MovieListPreviewParameterProvider::class) movieList: MovieList -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsMovieListBox( - currentMovieList = movieList, - onMovieListSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt deleted file mode 100644 index 00d268df8..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt +++ /dev/null @@ -1,161 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.RadioButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import org.michaelbel.movies.common.list.MovieList -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.listText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.accessibility.MoviesContentDescription -import org.michaelbel.movies.ui.icons.MoviesIcons -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.MovieListPreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsMovieListDialog( - currentMovieList: MovieList, - onMovieListSelect: (MovieList) -> Unit, - onDismissRequest: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = onDismissRequest, - modifier = Modifier.testTag("ConfirmTextButton") - ) { - Text( - text = stringResource(R.string.settings_action_cancel), - modifier = Modifier.testTag("ConfirmText"), - style = MaterialTheme.typography.labelLarge.copy(MaterialTheme.colorScheme.primary) - ) - } - }, - icon = { - Icon( - imageVector = MoviesIcons.LocalMovies, - contentDescription = MoviesContentDescription.None, - modifier = Modifier.testTag("Icon") - ) - }, - title = { - Text( - text = stringResource(R.string.settings_movie_list), - modifier = Modifier.testTag("Title"), - style = MaterialTheme.typography.headlineSmall.copy(MaterialTheme.colorScheme.onSurface) - ) - }, - text = { - SettingMovieListDialogContent( - currentMovieList = currentMovieList, - onMovieListSelect = { movieList -> - onMovieListSelect(movieList) - onDismissRequest() - }, - modifier = Modifier.testTag("Content") - ) - }, - shape = RoundedCornerShape(28.dp), - containerColor = MaterialTheme.colorScheme.surface, - iconContentColor = MaterialTheme.colorScheme.secondary, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -private fun SettingMovieListDialogContent( - currentMovieList: MovieList, - onMovieListSelect: (MovieList) -> Unit, - modifier: Modifier = Modifier -) { - val movieLists = listOf( - MovieList.NowPlaying, - MovieList.Popular, - MovieList.TopRated, - MovieList.Upcoming - ) - val scrollState = rememberScrollState() - - Column( - modifier = modifier.verticalScroll(scrollState) - ) { - movieLists.forEach { movieList -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .clickable { onMovieListSelect(movieList) }, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = currentMovieList == movieList, - onClick = null, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6F) - ), - modifier = Modifier.padding(start = 16.dp) - ) - - Text( - text = movieList.listText, - modifier = Modifier.padding(start = 8.dp), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } - } - } -} - -@Composable -@DevicePreviews -private fun SettingsMovieListDialogPreview( - @PreviewParameter(MovieListPreviewParameterProvider::class) movieList: MovieList -) { - MoviesTheme { - SettingsMovieListDialog( - currentMovieList = movieList, - onMovieListSelect = {}, - onDismissRequest = {} - ) - } -} - -@Composable -@Preview -private fun SettingsMovieListDialogAmoledPreview( - @PreviewParameter(MovieListPreviewParameterProvider::class) movieList: MovieList -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsMovieListDialog( - currentMovieList = movieList, - onMovieListSelect = {}, - onDismissRequest = {} - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt deleted file mode 100644 index 38d919401..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt +++ /dev/null @@ -1,109 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import android.Manifest -import android.app.Activity -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.SwitchDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.ktx.notificationManager -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.compose.SwitchCheckIcon -import org.michaelbel.movies.ui.ktx.appNotificationSettingsIntent -import org.michaelbel.movies.ui.lifecycle.OnResume - -@Composable -internal fun SettingsPostNotificationsBox( - onShowPermissionSnackbar: () -> Unit, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - val notificationManager = context.notificationManager - var areNotificationsEnabled by remember { mutableStateOf(notificationManager.areNotificationsEnabled()) } - val resultContract = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {} - - val postNotificationsPermission = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - areNotificationsEnabled = notificationManager.areNotificationsEnabled() - } else { - val shouldRequest = (context as Activity).shouldShowRequestPermissionRationale( - Manifest.permission.POST_NOTIFICATIONS - ) - if (!shouldRequest) { - onShowPermissionSnackbar() - } - } - } - - OnResume { - areNotificationsEnabled = notificationManager.areNotificationsEnabled() - } - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { - if (areNotificationsEnabled) { - resultContract.launch(context.appNotificationSettingsIntent) - } else { - postNotificationsPermission.launch(Manifest.permission.POST_NOTIFICATIONS) - } - } - .testTag("ConstraintLayout") - ) { - val (title, value) = createRefs() - - Text( - text = stringResource(R.string.settings_post_notifications), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("Text"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - Switch( - checked = areNotificationsEnabled, - onCheckedChange = null, - modifier = Modifier - .constrainAs(value) { - width = Dimension.wrapContent - height = Dimension.wrapContent - top.linkTo(parent.top) - end.linkTo(parent.end, 16.dp) - bottom.linkTo(parent.bottom) - } - .testTag("Switch"), - thumbContent = if (areNotificationsEnabled) { { SwitchCheckIcon() } } else null, - colors = SwitchDefaults.colors( - checkedTrackColor = MaterialTheme.colorScheme.surfaceTint, - checkedIconColor = MaterialTheme.colorScheme.surfaceTint - ) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsReviewBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsReviewBox.kt deleted file mode 100644 index ee0391354..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsReviewBox.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsReviewBox( - modifier: Modifier = Modifier -) { - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .testTag("ConstraintLayout") - ) { - val (title) = createRefs() - - Text( - text = stringResource(R.string.settings_review), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("Text"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsReviewBoxPreview() { - MoviesTheme { - SettingsReviewBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsReviewBoxAmoledPreview() { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsReviewBox( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt index f881f6da2..2196a9187 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt @@ -1,10 +1,14 @@ package org.michaelbel.movies.settings.ui +import android.Manifest import android.app.Activity +import android.app.GrammaticalInflectionManager +import android.appwidget.AppWidgetManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -19,34 +23,55 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch +import org.michaelbel.movies.common.MOVIES_GITHUB_URL import org.michaelbel.movies.common.appearance.FeedView +import org.michaelbel.movies.common.browser.openUrl +import org.michaelbel.movies.common.gender.GrammaticalGender +import org.michaelbel.movies.common.ktx.notificationManager import org.michaelbel.movies.common.list.MovieList import org.michaelbel.movies.common.localization.model.AppLanguage import org.michaelbel.movies.common.theme.AppTheme import org.michaelbel.movies.common.version.AppVersionData import org.michaelbel.movies.settings.SettingsViewModel import org.michaelbel.movies.settings.ktx.iconSnackbarText +import org.michaelbel.movies.settings.ktx.stringText +import org.michaelbel.movies.settings.ui.common.SettingAppIcon +import org.michaelbel.movies.settings.ui.common.SettingItem +import org.michaelbel.movies.settings.ui.common.SettingSwitchItem +import org.michaelbel.movies.settings.ui.common.SettingsDialog import org.michaelbel.movies.settings_impl.R +import org.michaelbel.movies.ui.appicon.IconAlias +import org.michaelbel.movies.ui.appicon.setIcon +import org.michaelbel.movies.ui.icons.MoviesIcons import org.michaelbel.movies.ui.ktx.appNotificationSettingsIntent import org.michaelbel.movies.ui.ktx.clickableWithoutRipple import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets +import org.michaelbel.movies.ui.lifecycle.OnResume import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme +import org.michaelbel.movies.widget.ktx.pin import org.michaelbel.movies.ui.R as UiR +import org.michaelbel.movies.widget.R as WidgetR @Composable fun SettingsRoute( @@ -77,6 +102,7 @@ fun SettingsRoute( dynamicColors = dynamicColors, onSetDynamicColors = viewModel::setDynamicColors, isPostNotificationsFeatureEnabled = viewModel.isPostNotificationsFeatureEnabled, + isReviewFeatureEnabled = viewModel.isReviewFeatureEnabled, isPlayServicesAvailable = isPlayServicesAvailable, onRequestReview = viewModel::requestReview, appVersionData = appVersionData, @@ -100,6 +126,7 @@ private fun SettingsScreenContent( dynamicColors: Boolean, onSetDynamicColors: (Boolean) -> Unit, isPostNotificationsFeatureEnabled: Boolean, + isReviewFeatureEnabled: Boolean, isPlayServicesAvailable: Boolean, onRequestReview: (Activity) -> Unit, appVersionData: AppVersionData, @@ -108,7 +135,10 @@ private fun SettingsScreenContent( val context = LocalContext.current val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + state = rememberTopAppBarState(), + canScroll = { true } + ) val lazyListState = rememberLazyListState() val resultContract = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {} @@ -140,13 +170,6 @@ private fun SettingsScreenContent( } } - fun onLaunchReviewFlow() { - when { - !isPlayServicesAvailable -> onShowSnackbar(context.getString(R.string.settings_error_play_services_not_available)) - else -> onRequestReview(context as Activity) - } - } - Scaffold( modifier = modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), topBar = { @@ -183,9 +206,24 @@ private fun SettingsScreenContent( contentPadding = innerPadding ) { item { - SettingsLanguageBox( - currentLanguage = currentLanguage, - onLanguageSelect = onLanguageSelect + var languageDialog by remember { mutableStateOf(false) } + + if (languageDialog) { + SettingsDialog( + icon = MoviesIcons.Language, + title = stringResource(R.string.settings_language), + items = AppLanguage.VALUES, + currentItem = currentLanguage, + onItemSelect = onLanguageSelect, + onDismissRequest = { languageDialog = false } + ) + } + + SettingItem( + title = stringResource(R.string.settings_language), + description = currentLanguage.stringText, + icon = MoviesIcons.Language, + onClick = { languageDialog = true } ) } item { @@ -196,9 +234,24 @@ private fun SettingsScreenContent( ) } item { - SettingsThemeBox( - currentTheme = currentTheme, - onThemeSelect = onThemeSelect + var themeDialog by remember { mutableStateOf(false) } + + if (themeDialog) { + SettingsDialog( + icon = painterResource(MoviesIcons.ThemeLightDark), + title = stringResource(R.string.settings_theme), + items = AppTheme.VALUES, + currentItem = currentTheme, + onItemSelect = onThemeSelect, + onDismissRequest = { themeDialog = false } + ) + } + + SettingItem( + title = stringResource(R.string.settings_theme), + description = currentTheme.stringText, + icon = painterResource(MoviesIcons.ThemeLightDark), + onClick = { themeDialog = true } ) } item { @@ -209,9 +262,24 @@ private fun SettingsScreenContent( ) } item { - SettingsAppearanceBox( - currentFeedView = currentFeedView, - onFeedViewSelect = onFeedViewSelect + var appearanceDialog by remember { mutableStateOf(false) } + + if (appearanceDialog) { + SettingsDialog( + icon = MoviesIcons.GridView, + title = stringResource(R.string.settings_appearance), + items = FeedView.VALUES, + currentItem = currentFeedView, + onItemSelect = onFeedViewSelect, + onDismissRequest = { appearanceDialog = false } + ) + } + + SettingItem( + title = stringResource(R.string.settings_appearance), + description = currentFeedView.stringText, + icon = MoviesIcons.GridView, + onClick = { appearanceDialog = true } ) } item { @@ -222,9 +290,24 @@ private fun SettingsScreenContent( ) } item { - SettingsMovieListBox( - currentMovieList = currentMovieList, - onMovieListSelect = onMovieListSelect + var movieListDialog by remember { mutableStateOf(false) } + + if (movieListDialog) { + SettingsDialog( + icon = MoviesIcons.LocalMovies, + title = stringResource(R.string.settings_movie_list), + items = MovieList.VALUES, + currentItem = currentMovieList, + onItemSelect = onMovieListSelect, + onDismissRequest = { movieListDialog = false } + ) + } + + SettingItem( + title = stringResource(R.string.settings_movie_list), + description = currentMovieList.stringText, + icon = MoviesIcons.LocalMovies, + onClick = { movieListDialog = true } ) } item { @@ -236,7 +319,30 @@ private fun SettingsScreenContent( } if (isGrammaticalGenderFeatureEnabled) { item { - SettingsGenderBox() + val grammaticalInflectionManager by remember { mutableStateOf(context.getSystemService(GrammaticalInflectionManager::class.java)) } + val grammaticalGender by remember { mutableStateOf(grammaticalInflectionManager.applicationGrammaticalGender) } + val currentGrammaticalGender by remember { mutableStateOf(GrammaticalGender.transform(grammaticalGender)) } + var genderDialog by remember { mutableStateOf(false) } + + if (genderDialog) { + SettingsDialog( + icon = painterResource(MoviesIcons.Cat), + title = stringResource(R.string.settings_gender), + items = GrammaticalGender.VALUES, + currentItem = currentGrammaticalGender, + onItemSelect = { item -> + grammaticalInflectionManager.setRequestedApplicationGrammaticalGender(item.value) + }, + onDismissRequest = { genderDialog = false } + ) + } + + SettingItem( + title = stringResource(R.string.settings_gender), + description = currentGrammaticalGender.stringText, + icon = painterResource(MoviesIcons.Cat), + onClick = { genderDialog = true } + ) } item { HorizontalDivider( @@ -248,9 +354,12 @@ private fun SettingsScreenContent( } if (isDynamicColorsFeatureEnabled) { item { - SettingsDynamicColorsBox( - isDynamicColorsEnabled = dynamicColors, - modifier = Modifier.clickable { onSetDynamicColors(!dynamicColors) } + SettingSwitchItem( + title = stringResource(R.string.settings_dynamic_colors), + description = stringResource(R.string.settings_dynamic_colors_description), + icon = MoviesIcons.Palette, + checked = dynamicColors, + onClick = { onSetDynamicColors(!dynamicColors) } ) } item { @@ -263,8 +372,38 @@ private fun SettingsScreenContent( } if (isPostNotificationsFeatureEnabled) { item { - SettingsPostNotificationsBox( - onShowPermissionSnackbar = onShowPermissionSnackbar + val notificationManager by remember { mutableStateOf(context.notificationManager) } + var areNotificationsEnabled by remember { mutableStateOf(notificationManager.areNotificationsEnabled()) } + + val postNotificationsPermission = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + areNotificationsEnabled = notificationManager.areNotificationsEnabled() + } else { + val shouldRequest = (context as Activity).shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) + if (!shouldRequest) { + onShowPermissionSnackbar() + } + } + } + + OnResume { + areNotificationsEnabled = notificationManager.areNotificationsEnabled() + } + + SettingSwitchItem( + title = stringResource(R.string.settings_post_notifications), + description = stringResource(if (areNotificationsEnabled) R.string.settings_post_notifications_granted else R.string.settings_post_notifications_denied), + icon = MoviesIcons.Notifications, + checked = areNotificationsEnabled, + onClick = { + if (areNotificationsEnabled) { + resultContract.launch(context.appNotificationSettingsIntent) + } else { + postNotificationsPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } ) } item { @@ -276,8 +415,14 @@ private fun SettingsScreenContent( } } item { - SettingsReviewBox( - modifier = Modifier.clickable { onLaunchReviewFlow() } + val appWidgetManager by remember { mutableStateOf(AppWidgetManager.getInstance(context)) } + val appWidgetProvider by remember { mutableStateOf(appWidgetManager.getInstalledProvidersForPackage(context.packageName, null).first()) } + + SettingItem( + title = stringResource(R.string.settings_app_widget), + description = stringResource(R.string.settings_app_widget_description, stringResource(WidgetR.string.appwidget_description)), + icon = MoviesIcons.Widgets, + onClick = { appWidgetProvider.pin(context) } ) } item { @@ -288,22 +433,49 @@ private fun SettingsScreenContent( ) } item { - SettingsAppIconBox( - onAppIconChanged = { iconAlias -> - onShowSnackbar(context.getString(R.string.settings_app_launcher_icon_changed_to, iconAlias.iconSnackbarText(context))) - }, - modifier = Modifier.fillMaxWidth() - ) - } - item { - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), - thickness = .1.dp, - color = MaterialTheme.colorScheme.onPrimaryContainer + Text( + text = stringResource(R.string.settings_app_launcher_icon), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.titleLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) ) } item { - SettingsAppWidgetBox() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + SettingAppIcon( + iconAlias = IconAlias.Red, + onClick = { icon -> + onShowSnackbar(context.getString(R.string.settings_app_launcher_icon_changed_to, icon.iconSnackbarText(context))) + context.setIcon(icon) + } + ) + + SettingAppIcon( + iconAlias = IconAlias.Purple, + onClick = { icon -> + onShowSnackbar(context.getString(R.string.settings_app_launcher_icon_changed_to, icon.iconSnackbarText(context))) + context.setIcon(icon) + } + ) + + SettingAppIcon( + iconAlias = IconAlias.Brown, + onClick = { icon -> + onShowSnackbar(context.getString(R.string.settings_app_launcher_icon_changed_to, icon.iconSnackbarText(context))) + context.setIcon(icon) + } + ) + + SettingAppIcon( + iconAlias = IconAlias.Amoled, + onClick = { icon -> + onShowSnackbar(context.getString(R.string.settings_app_launcher_icon_changed_to, icon.iconSnackbarText(context))) + context.setIcon(icon) + } + ) + } } item { HorizontalDivider( @@ -313,7 +485,38 @@ private fun SettingsScreenContent( ) } item { - SettingsGithubBox() + val toolbarColor = MaterialTheme.colorScheme.primary.toArgb() + + SettingItem( + title = stringResource(R.string.settings_github), + description = stringResource(R.string.settings_github_description), + icon = painterResource(MoviesIcons.Github), + onClick = { openUrl(resultContract, toolbarColor, MOVIES_GITHUB_URL) } + ) + } + if (isReviewFeatureEnabled) { + item { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + thickness = .1.dp, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + item { + fun onLaunchReviewFlow() { + when { + !isPlayServicesAvailable -> onShowSnackbar(context.getString(R.string.settings_error_play_services_not_available)) + else -> onRequestReview(context as Activity) + } + } + + SettingItem( + title = stringResource(R.string.settings_review), + description = stringResource(R.string.settings_review_description), + icon = painterResource(MoviesIcons.GooglePlay), + onClick = { onLaunchReviewFlow() } + ) + } } } } @@ -339,6 +542,7 @@ private fun SettingsScreenContentPreview() { onSetDynamicColors = {}, isPostNotificationsFeatureEnabled = true, isPlayServicesAvailable = true, + isReviewFeatureEnabled = true, onRequestReview = {}, appVersionData = AppVersionData.Empty, modifier = Modifier @@ -371,6 +575,7 @@ private fun SettingsScreenContentAmoledPreview() { onSetDynamicColors = {}, isPostNotificationsFeatureEnabled = true, isPlayServicesAvailable = true, + isReviewFeatureEnabled = true, onRequestReview = {}, appVersionData = AppVersionData.Empty, modifier = Modifier diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt deleted file mode 100644 index 404f4ae91..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt +++ /dev/null @@ -1,122 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.themeText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.ThemePreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingsThemeBox( - currentTheme: AppTheme, - onThemeSelect: (AppTheme) -> Unit, - modifier: Modifier = Modifier -) { - var themeDialog by remember { mutableStateOf(false) } - - if (themeDialog) { - SettingThemeDialog( - currentTheme = currentTheme, - onThemeSelect = onThemeSelect, - onDismissRequest = { - themeDialog = false - } - ) - } - - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clickable { - themeDialog = true - } - .testTag("ConstraintLayout") - ) { - val (title, value) = createRefs() - - Text( - text = stringResource(R.string.settings_theme), - modifier = Modifier - .constrainAs(title) { - width = Dimension.wrapContent - height = Dimension.wrapContent - start.linkTo(parent.start, 16.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - } - .testTag("TitleText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - - Text( - text = currentTheme.themeText, - modifier = Modifier - .constrainAs(value) { - width = Dimension.wrapContent - height = Dimension.wrapContent - top.linkTo(parent.top) - end.linkTo(parent.end, 16.dp) - bottom.linkTo(parent.bottom) - } - .testTag("ValueText"), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.primary) - ) - } -} - -@Composable -@DevicePreviews -private fun SettingsThemeBoxPreview( - @PreviewParameter(ThemePreviewParameterProvider::class) theme: AppTheme -) { - MoviesTheme { - SettingsThemeBox( - currentTheme = theme, - onThemeSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} - -@Composable -@Preview -private fun SettingsThemeBoxAmoledPreview( - @PreviewParameter(ThemePreviewParameterProvider::class) theme: AppTheme -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingsThemeBox( - currentTheme = theme, - onThemeSelect = {}, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt deleted file mode 100644 index 8032628f8..000000000 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt +++ /dev/null @@ -1,155 +0,0 @@ -package org.michaelbel.movies.settings.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.RadioButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import org.michaelbel.movies.common.theme.AppTheme -import org.michaelbel.movies.settings.ktx.themeText -import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.accessibility.MoviesContentDescription -import org.michaelbel.movies.ui.icons.MoviesIcons -import org.michaelbel.movies.ui.preview.DevicePreviews -import org.michaelbel.movies.ui.preview.provider.ThemePreviewParameterProvider -import org.michaelbel.movies.ui.theme.MoviesTheme - -@Composable -internal fun SettingThemeDialog( - currentTheme: AppTheme, - onThemeSelect: (AppTheme) -> Unit, - onDismissRequest: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = onDismissRequest, - modifier = Modifier.testTag("ConfirmTextButton") - ) { - Text( - text = stringResource(R.string.settings_action_cancel), - modifier = Modifier.testTag("ConfirmText"), - style = MaterialTheme.typography.labelLarge.copy(MaterialTheme.colorScheme.primary) - ) - } - }, - icon = { - Icon( - painter = painterResource(MoviesIcons.ThemeLightDark), - contentDescription = stringResource(MoviesContentDescription.ThemeIcon), - modifier = Modifier.testTag("Icon") - ) - }, - title = { - Text( - text = stringResource(R.string.settings_theme), - modifier = Modifier.testTag("Title"), - style = MaterialTheme.typography.headlineSmall.copy(MaterialTheme.colorScheme.onSurface) - ) - }, - text = { - SettingThemeDialogContent( - currentTheme = currentTheme, - onThemeSelect = { theme -> - onThemeSelect(theme) - onDismissRequest() - }, - modifier = Modifier.testTag("Content") - ) - }, - shape = RoundedCornerShape(28.dp), - containerColor = MaterialTheme.colorScheme.surface, - iconContentColor = MaterialTheme.colorScheme.secondary, - titleContentColor = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -private fun SettingThemeDialogContent( - currentTheme: AppTheme, - onThemeSelect: (AppTheme) -> Unit, - modifier: Modifier = Modifier -) { - val scrollState = rememberScrollState() - - Column( - modifier = modifier.verticalScroll(scrollState) - ) { - AppTheme.VALUES.forEach { theme -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .clickable { onThemeSelect(theme) }, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = currentTheme == theme, - onClick = null, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6F) - ), - modifier = Modifier.padding(start = 16.dp) - ) - - Text( - text = theme.themeText, - modifier = Modifier.padding(start = 8.dp), - style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } - } - } -} - -@Composable -@DevicePreviews -private fun SettingThemeDialogPreview( - @PreviewParameter(ThemePreviewParameterProvider::class) theme: AppTheme -) { - MoviesTheme { - SettingThemeDialog( - currentTheme = theme, - onThemeSelect = {}, - onDismissRequest = {} - ) - } -} - -@Composable -@Preview -private fun SettingThemeDialogAmoledPreview( - @PreviewParameter(ThemePreviewParameterProvider::class) theme: AppTheme -) { - MoviesTheme( - theme = AppTheme.Amoled - ) { - SettingThemeDialog( - currentTheme = theme, - onThemeSelect = {}, - onDismissRequest = {} - ) - } -} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt index cfcfbf068..edc11498d 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt @@ -2,16 +2,15 @@ package org.michaelbel.movies.settings.ui import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import org.michaelbel.movies.common.theme.AppTheme import org.michaelbel.movies.settings_impl.R @@ -26,13 +25,12 @@ internal fun SettingsToolbar( topAppBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), onNavigationIconClick: () -> Unit, ) { - TopAppBar( + LargeTopAppBar( title = { Text( text = stringResource(R.string.settings_title), modifier = Modifier.testTag("TitleText"), - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) + color = MaterialTheme.colorScheme.onPrimaryContainer ) }, modifier = modifier.testTag("TopAppBar"), diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingAppIcon.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingAppIcon.kt new file mode 100644 index 000000000..8657cd997 --- /dev/null +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingAppIcon.kt @@ -0,0 +1,124 @@ +package org.michaelbel.movies.settings.ui.common + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import org.michaelbel.movies.common.theme.AppTheme +import org.michaelbel.movies.ui.accessibility.MoviesContentDescription +import org.michaelbel.movies.ui.appicon.IconAlias +import org.michaelbel.movies.ui.appicon.isEnabled +import org.michaelbel.movies.ui.icons.MoviesIcons +import org.michaelbel.movies.ui.preview.DevicePreviews +import org.michaelbel.movies.ui.preview.provider.IconAliasPreviewParameterProvider +import org.michaelbel.movies.ui.theme.MoviesTheme + +@Composable +fun RowScope.SettingAppIcon( + iconAlias: IconAlias, + onClick: (IconAlias) -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + + val containerSize by animateDpAsState( + targetValue = if (context.isEnabled(iconAlias)) 28.dp else 0.dp, + label = "containerSize" + ) + val iconSize by animateDpAsState( + targetValue = if (context.isEnabled(iconAlias)) 16.dp else 0.dp, + label = "iconSize" + ) + + Box( + modifier = modifier + .padding(4.dp) + .sizeIn(maxHeight = 80.dp, maxWidth = 80.dp, minHeight = 64.dp, minWidth = 64.dp) + .weight(1F, false) + .aspectRatio(1F) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.inversePrimary) + .clickable { onClick(iconAlias) } + ) { + Icon( + painter = painterResource(iconAlias.iconRes), + contentDescription = stringResource(MoviesContentDescription.AppIcon), + modifier = Modifier + .padding(8.dp) + .clip(CircleShape) + .align(Alignment.Center), + tint = Color.Unspecified + ) + + Box( + modifier = Modifier + .align(Alignment.Center) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer) + .size(containerSize) + ) { + Icon( + imageVector = MoviesIcons.Check, + contentDescription = MoviesContentDescription.None, + modifier = Modifier + .size(iconSize) + .align(Alignment.Center), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } +} + +@Composable +@DevicePreviews +private fun SettingAppIconPreview( + @PreviewParameter(IconAliasPreviewParameterProvider::class) iconAlias: IconAlias +) { + MoviesTheme { + Row { + SettingAppIcon( + iconAlias = iconAlias, + onClick = {} + ) + } + } +} + +@Composable +@Preview +private fun SettingAppIconAmoledPreview( + @PreviewParameter(IconAliasPreviewParameterProvider::class) iconAlias: IconAlias +) { + MoviesTheme( + theme = AppTheme.Amoled + ) { + Row { + SettingAppIcon( + iconAlias = iconAlias, + onClick = {} + ) + } + } +} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingDialog.kt new file mode 100644 index 000000000..17542d6e9 --- /dev/null +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingDialog.kt @@ -0,0 +1,239 @@ +package org.michaelbel.movies.settings.ui.common + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import org.michaelbel.movies.common.SealedString +import org.michaelbel.movies.common.localization.model.AppLanguage +import org.michaelbel.movies.common.theme.AppTheme +import org.michaelbel.movies.settings.ktx.stringText +import org.michaelbel.movies.settings_impl.R +import org.michaelbel.movies.ui.accessibility.MoviesContentDescription +import org.michaelbel.movies.ui.icons.MoviesIcons +import org.michaelbel.movies.ui.preview.DevicePreviews +import org.michaelbel.movies.ui.preview.provider.AppearancePreviewParameterProvider +import org.michaelbel.movies.ui.preview.provider.LanguagePreviewParameterProvider +import org.michaelbel.movies.ui.theme.MoviesTheme + +@Composable +internal fun SettingsDialog( + icon: ImageVector, + title: String, + items: List, + currentItem: T, + onItemSelect: (T) -> Unit, + onDismissRequest: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = onDismissRequest, + modifier = Modifier.testTag("ConfirmTextButton") + ) { + Text( + text = stringResource(R.string.settings_action_cancel), + modifier = Modifier.testTag("ConfirmText"), + style = MaterialTheme.typography.labelLarge.copy(MaterialTheme.colorScheme.primary) + ) + } + }, + icon = { + Icon( + imageVector = icon, + contentDescription = stringResource(MoviesContentDescription.AppearanceIcon), + modifier = Modifier.testTag("Icon") + ) + }, + title = { + Text( + text = title, + modifier = Modifier.testTag("Title"), + style = MaterialTheme.typography.headlineSmall.copy(MaterialTheme.colorScheme.onSurface) + ) + }, + text = { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier.verticalScroll(scrollState) + ) { + items.forEach { item -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable { + onItemSelect(item) + onDismissRequest() + }, + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = currentItem == item, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.primary, + unselectedColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = .6F) + ), + modifier = Modifier.padding(start = 16.dp) + ) + + Text( + text = item.stringText, + modifier = Modifier.padding(start = 8.dp), + style = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + } + } + }, + shape = RoundedCornerShape(28.dp), + containerColor = MaterialTheme.colorScheme.surface, + iconContentColor = MaterialTheme.colorScheme.secondary, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) +} + +@Composable +internal fun SettingsDialog( + icon: Painter, + title: String, + items: List, + currentItem: T, + onItemSelect: (T) -> Unit, + onDismissRequest: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = onDismissRequest, + modifier = Modifier.testTag("ConfirmTextButton") + ) { + Text( + text = stringResource(R.string.settings_action_cancel), + modifier = Modifier.testTag("ConfirmText"), + style = MaterialTheme.typography.labelLarge.copy(MaterialTheme.colorScheme.primary) + ) + } + }, + icon = { + Icon( + painter = icon, + contentDescription = stringResource(MoviesContentDescription.AppearanceIcon), + modifier = Modifier.testTag("Icon") + ) + }, + title = { + Text( + text = title, + modifier = Modifier.testTag("Title"), + style = MaterialTheme.typography.headlineSmall.copy(MaterialTheme.colorScheme.onSurface) + ) + }, + text = { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier.verticalScroll(scrollState) + ) { + items.forEach { item -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .clickable { + onItemSelect(item) + onDismissRequest() + }, + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = currentItem == item, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colorScheme.primary, + unselectedColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = .6F) + ), + modifier = Modifier.padding(start = 16.dp) + ) + + Text( + text = item.stringText, + modifier = Modifier.padding(start = 8.dp), + style = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + } + } + }, + shape = RoundedCornerShape(28.dp), + containerColor = MaterialTheme.colorScheme.surface, + iconContentColor = MaterialTheme.colorScheme.secondary, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) +} + +@Composable +@DevicePreviews +private fun SettingDialogPreview( + @PreviewParameter(AppearancePreviewParameterProvider::class) appLanguage: AppLanguage +) { + MoviesTheme { + SettingsDialog( + icon = MoviesIcons.Language, + title = stringResource(R.string.settings_language), + items = AppLanguage.VALUES, + currentItem = appLanguage, + onItemSelect = {}, + onDismissRequest = {} + ) + } +} + +@Composable +@Preview +private fun SettingDialogAmoledPreview( + @PreviewParameter(LanguagePreviewParameterProvider::class) appLanguage: AppLanguage +) { + MoviesTheme( + theme = AppTheme.Amoled + ) { + SettingsDialog( + icon = MoviesIcons.Language, + title = stringResource(R.string.settings_language), + items = AppLanguage.VALUES, + currentItem = appLanguage, + onItemSelect = {}, + onDismissRequest = {} + ) + } +} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingItem.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingItem.kt new file mode 100644 index 000000000..cc1986545 --- /dev/null +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingItem.kt @@ -0,0 +1,134 @@ +package org.michaelbel.movies.settings.ui.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.michaelbel.movies.common.theme.AppTheme +import org.michaelbel.movies.ui.accessibility.MoviesContentDescription +import org.michaelbel.movies.ui.icons.MoviesIcons +import org.michaelbel.movies.ui.preview.DevicePreviews +import org.michaelbel.movies.ui.theme.MoviesTheme + +@Composable +internal fun SettingItem( + title: String, + description: String, + icon: ImageVector, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = 8.dp, vertical = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = MoviesContentDescription.None, + modifier = Modifier + .padding(start = 8.dp, end = 16.dp) + .size(24.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + + Column( + modifier = Modifier.weight(1F) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) + ) + + Text( + text = description, + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.primary) + ) + } + } +} + +@Composable +internal fun SettingItem( + title: String, + description: String, + icon: Painter, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = 8.dp, vertical = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = icon, + contentDescription = MoviesContentDescription.None, + modifier = Modifier + .padding(start = 8.dp, end = 16.dp) + .size(24.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + + Column( + modifier = Modifier.weight(1F) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) + ) + + Text( + text = description, + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.primary) + ) + } + } +} + +@Composable +@DevicePreviews +private fun SettingItemPreview() { + MoviesTheme { + SettingItem( + title = "Title", + description = "Description", + icon = MoviesIcons.Language, + onClick = {}, + modifier = Modifier.background(MaterialTheme.colorScheme.primaryContainer) + ) + } +} + +@Composable +@Preview +private fun SettingItemAmoledPreview() { + MoviesTheme( + theme = AppTheme.Amoled + ) { + SettingItem( + title = "Title", + description = "Description", + icon = MoviesIcons.Language, + onClick = {}, + modifier = Modifier.background(MaterialTheme.colorScheme.primaryContainer) + ) + } +} \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingSwitchItem.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingSwitchItem.kt new file mode 100644 index 000000000..31be76af1 --- /dev/null +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/common/SettingSwitchItem.kt @@ -0,0 +1,209 @@ +package org.michaelbel.movies.settings.ui.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import org.michaelbel.movies.common.theme.AppTheme +import org.michaelbel.movies.ui.accessibility.MoviesContentDescription +import org.michaelbel.movies.ui.compose.SwitchCheckIcon +import org.michaelbel.movies.ui.icons.MoviesIcons +import org.michaelbel.movies.ui.preview.DevicePreviews +import org.michaelbel.movies.ui.preview.provider.BooleanPreviewParameterProvider +import org.michaelbel.movies.ui.theme.MoviesTheme + +@Composable +internal fun SettingSwitchItem( + title: String, + description: String, + icon: ImageVector, + checked: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(vertical = 20.dp) + ) { + val (iconRef, texts, switch) = createRefs() + + Icon( + imageVector = icon, + contentDescription = MoviesContentDescription.None, + modifier = Modifier.constrainAs(iconRef) { + width = Dimension.value(24.dp) + height = Dimension.value(24.dp) + start.linkTo(parent.start, 16.dp) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + + Column( + modifier = Modifier.constrainAs(texts) { + width = Dimension.fillToConstraints + height = Dimension.wrapContent + start.linkTo(iconRef.end, 16.dp) + top.linkTo(parent.top) + end.linkTo(switch.start, 16.dp) + bottom.linkTo(parent.bottom) + } + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) + ) + + Text( + text = description, + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.primary) + ) + } + + Switch( + checked = checked, + onCheckedChange = null, + modifier = Modifier + .constrainAs(switch) { + width = Dimension.wrapContent + height = Dimension.wrapContent + top.linkTo(parent.top) + end.linkTo(parent.end, 16.dp) + bottom.linkTo(parent.bottom) + } + .testTag("Switch"), + thumbContent = if (checked) { { SwitchCheckIcon() } } else null, + colors = SwitchDefaults.colors( + checkedTrackColor = MaterialTheme.colorScheme.surfaceTint, + checkedIconColor = MaterialTheme.colorScheme.surfaceTint + ) + ) + } +} + +@Composable +internal fun SettingSwitchItem( + title: String, + description: String, + icon: Painter, + checked: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = 8.dp, vertical = 20.dp) + ) { + val (iconRef, texts, switch) = createRefs() + + Icon( + painter = icon, + contentDescription = MoviesContentDescription.None, + modifier = Modifier.constrainAs(iconRef) { + width = Dimension.value(24.dp) + height = Dimension.value(24.dp) + start.linkTo(parent.start, 16.dp) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + + Column( + modifier = Modifier.constrainAs(texts) { + width = Dimension.fillToConstraints + height = Dimension.wrapContent + start.linkTo(iconRef.end, 16.dp) + top.linkTo(parent.top) + end.linkTo(switch.start, 16.dp) + bottom.linkTo(parent.bottom) + } + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy(MaterialTheme.colorScheme.onPrimaryContainer) + ) + + Text( + text = description, + style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.primary) + ) + } + + Switch( + checked = checked, + onCheckedChange = null, + modifier = Modifier + .constrainAs(switch) { + width = Dimension.wrapContent + height = Dimension.wrapContent + top.linkTo(parent.top) + end.linkTo(parent.end, 16.dp) + bottom.linkTo(parent.bottom) + } + .testTag("Switch"), + thumbContent = if (checked) { { SwitchCheckIcon() } } else null, + colors = SwitchDefaults.colors( + checkedTrackColor = MaterialTheme.colorScheme.surfaceTint, + checkedIconColor = MaterialTheme.colorScheme.surfaceTint + ) + ) + } +} + +@Composable +@DevicePreviews +private fun SettingSwitchItemPreview( + @PreviewParameter(BooleanPreviewParameterProvider::class) checked: Boolean +) { + MoviesTheme { + SettingSwitchItem( + title = "Title", + description = "Description", + icon = MoviesIcons.Language, + checked = checked, + onClick = {}, + modifier = Modifier.background(MaterialTheme.colorScheme.primaryContainer) + ) + } +} + +@Composable +@Preview +private fun SettingSwitchItemAmoledPreview( + @PreviewParameter(BooleanPreviewParameterProvider::class) checked: Boolean +) { + MoviesTheme( + theme = AppTheme.Amoled + ) { + SettingSwitchItem( + title = "Title", + description = "Description", + icon = MoviesIcons.Language, + checked = checked, + onClick = {}, + modifier = Modifier.background(MaterialTheme.colorScheme.primaryContainer) + ) + } +} \ No newline at end of file diff --git a/feature/settings-impl/src/main/res/values-ru/strings.xml b/feature/settings-impl/src/main/res/values-ru/strings.xml index a60521f37..fb3169d54 100644 --- a/feature/settings-impl/src/main/res/values-ru/strings.xml +++ b/feature/settings-impl/src/main/res/values-ru/strings.xml @@ -6,8 +6,12 @@ Темная Amoled Цвета обоев + Извлечь цвета из обоев Уведомления + Разрешение отклонено + Разрешение предоставлено Оценить приложение + Используя In-App Review API Уведомления отключены. Активируйте их в настройках приложения Перейти Сервисы Google play недоступны @@ -24,17 +28,19 @@ Топ Скоро Иконка Приложения - Красная - Фиолетовая - Коричневая + Красную + Фиолетовую + Коричневую Amoled Иконка приложения изменена на %s Movies v%s (%s) Debug Отмена - Movies на Github + Movies на GitHub + Посмотреть GitHub-репозиторий Установить виджет + 3x2 %s Род Не указан Средний diff --git a/feature/settings-impl/src/main/res/values/strings.xml b/feature/settings-impl/src/main/res/values/strings.xml index dfcc24fef..82a0a0be6 100644 --- a/feature/settings-impl/src/main/res/values/strings.xml +++ b/feature/settings-impl/src/main/res/values/strings.xml @@ -6,8 +6,12 @@ Dark Amoled Dynamic Colors + Apply colors from wallpaper Notifications - Review + Permission Denied + Permission Granted + Rate App + Using In-App Review API Notifications are disabled. Please go to app settings to activate them Go Google play services are not available @@ -33,8 +37,10 @@ (%s) Debug Cancel - Movies on Github + Movies on GitHub + Check the GitHub repository Install AppWidget + 3x2 %s Gender Not specified Neutral