Переглянути джерело

Added theme radio buttons instead of dark theme checkbox

Vadik Sirekanyan 5 роки тому
батько
коміт
34fdb95bf5

+ 7 - 0
app/src/main/java/com/sirekanyan/knigopis/App.kt

@@ -4,6 +4,7 @@ import android.app.Application
 import com.sirekanyan.knigopis.dependency.*
 
 class App : Application() {
+
     val config by lazy(::provideConfig)
     val resourceProvider by lazy(::provideResources)
     val tokenStorage by lazy(::provideTokenStorage)
@@ -15,4 +16,10 @@ class App : Application() {
     val endpoint by lazy(::provideEndpoint)
     val cache by lazy(::provideCache)
     val gson by lazy(::provideGson)
+
+    override fun onCreate() {
+        super.onCreate()
+        config.theme.setup()
+    }
+
 }

+ 4 - 7
app/src/main/java/com/sirekanyan/knigopis/common/extensions/Context.kt

@@ -3,13 +3,15 @@ package com.sirekanyan.knigopis.common.extensions
 import android.content.ClipboardManager
 import android.content.Context
 import android.content.Intent
+import android.content.res.Configuration.UI_MODE_NIGHT_MASK
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
 import android.net.ConnectivityManager
 import android.view.inputmethod.InputMethodManager
 import android.widget.Toast
 import androidx.annotation.StringRes
-import com.sirekanyan.knigopis.R
 
-var isDarkTheme = false
+val Context.isNightMode: Boolean
+    get() = resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
 
 val Context.systemClipboardManager: ClipboardManager
     get() = getAndroidSystemService(Context.CLIPBOARD_SERVICE)
@@ -33,10 +35,5 @@ fun Context.showToast(@StringRes messageId: Int) {
     Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show()
 }
 
-fun Context.setDarkTheme(isDark: Boolean) {
-    setTheme(if (isDark) R.style.DarkAppTheme else R.style.AppTheme)
-    isDarkTheme = isDark
-}
-
 private inline fun <reified T> Context.getAndroidSystemService(name: String) =
     getSystemService(name) as T

+ 1 - 16
app/src/main/java/com/sirekanyan/knigopis/feature/MainActivity.kt

@@ -8,7 +8,6 @@ import android.os.Bundle
 import com.sirekanyan.knigopis.R
 import com.sirekanyan.knigopis.common.BaseActivity
 import com.sirekanyan.knigopis.common.extensions.*
-import com.sirekanyan.knigopis.common.functions.extra
 import com.sirekanyan.knigopis.common.functions.logError
 import com.sirekanyan.knigopis.dependency.providePresenter
 import com.sirekanyan.knigopis.feature.book.createBookIntent
@@ -23,7 +22,6 @@ import com.sirekanyan.knigopis.feature.users.saveMainState
 import com.sirekanyan.knigopis.model.*
 
 private const val BOOK_REQUEST_CODE = 1
-private val CURRENT_TAB_EXTRA = extra("current_tab")
 
 fun Context.startMainActivity() {
     startActivity(
@@ -46,10 +44,7 @@ class MainActivity : BaseActivity(),
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val restoredCurrentTab = savedInstanceState?.getMainState()?.currentTab
-        val currentTab = intent.getIntExtra(CURRENT_TAB_EXTRA, 0)
-            .takeUnless { it == 0 }
-            ?.let { CurrentTab.getByItemId(it) }
-        presenter.init(restoredCurrentTab ?: currentTab)
+        presenter.init(restoredCurrentTab)
     }
 
     override fun onStart() {
@@ -134,14 +129,4 @@ class MainActivity : BaseActivity(),
         startActivityOrNull(Intent(ACTION_VIEW, uri)) ?: showToast(R.string.users_info_no_browser)
     }
 
-    override fun reopenScreen() {
-        finish()
-        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
-        startActivity(intent.also { intent ->
-            presenter.state?.let { state ->
-                intent.putExtra(CURRENT_TAB_EXTRA, state.currentTab.itemId)
-            }
-        })
-    }
-
 }

+ 5 - 5
app/src/main/java/com/sirekanyan/knigopis/feature/MainPresenter.kt

@@ -9,6 +9,7 @@ import com.sirekanyan.knigopis.model.CurrentTab.BOOKS_TAB
 import com.sirekanyan.knigopis.model.CurrentTab.NOTES_TAB
 import com.sirekanyan.knigopis.repository.AuthRepository
 import com.sirekanyan.knigopis.repository.Configuration
+import com.sirekanyan.knigopis.repository.Theme
 
 interface MainPresenter : Presenter {
 
@@ -22,7 +23,6 @@ interface MainPresenter : Presenter {
     interface Router {
         fun openLoginScreen()
         fun openProfileScreen()
-        fun reopenScreen()
     }
 
 }
@@ -46,7 +46,7 @@ class MainPresenterImpl(
         get() = currentTab?.let { MainPresenterState(it) }
 
     override fun init(tab: CurrentTab?) {
-        view.setDarkThemeOptionChecked(config.isDarkTheme)
+        view.setThemeOptionChecked(Theme.getCurrent())
         val defaultTab = if (auth.isAuthorized()) BOOKS_TAB else NOTES_TAB
         this.currentTab = tab ?: defaultTab
     }
@@ -133,9 +133,9 @@ class MainPresenterImpl(
         view.showAboutDialog()
     }
 
-    override fun onDarkThemeOptionClicked(isChecked: Boolean) {
-        config.isDarkTheme = isChecked
-        router.reopenScreen()
+    override fun onThemeOptionClicked(theme: Theme) {
+        config.theme = theme
+        theme.setup()
     }
 
     override fun onRefreshSwiped() {

+ 22 - 11
app/src/main/java/com/sirekanyan/knigopis/feature/MainView.kt

@@ -7,9 +7,11 @@ import androidx.appcompat.app.AlertDialog
 import com.sirekanyan.knigopis.BuildConfig
 import com.sirekanyan.knigopis.R
 import com.sirekanyan.knigopis.common.extensions.hide
+import com.sirekanyan.knigopis.common.extensions.isNightMode
 import com.sirekanyan.knigopis.common.extensions.show
 import com.sirekanyan.knigopis.model.CurrentTab
 import com.sirekanyan.knigopis.model.CurrentTab.*
+import com.sirekanyan.knigopis.repository.Theme
 import com.sirekanyan.knigopis.repository.cache.COMMON_PREFS_NAME
 import kotlinx.android.extensions.LayoutContainer
 import kotlinx.android.synthetic.main.about.view.*
@@ -19,6 +21,8 @@ import kotlinx.android.synthetic.main.default_app_bar.*
 import kotlinx.android.synthetic.main.notes_page.*
 import kotlinx.android.synthetic.main.users_page.*
 
+private val DEBUG_OPTIONS = arrayOf(R.id.debug_option_clear_cache, R.id.debug_option_toggle_theme)
+
 interface MainView {
 
     fun showAboutDialog()
@@ -27,7 +31,7 @@ interface MainView {
     fun setNavigation(itemId: Int)
     fun showLoginOption(isVisible: Boolean)
     fun showProfileOption(isVisible: Boolean)
-    fun setDarkThemeOptionChecked(isChecked: Boolean)
+    fun setThemeOptionChecked(theme: Theme)
 
     interface Callbacks {
         fun onNavigationClicked(itemId: Int)
@@ -35,7 +39,7 @@ interface MainView {
         fun onLoginOptionClicked()
         fun onProfileOptionClicked()
         fun onAboutOptionClicked()
-        fun onDarkThemeOptionClicked(isChecked: Boolean)
+        fun onThemeOptionClicked(theme: Theme)
     }
 
 }
@@ -48,7 +52,6 @@ class MainViewImpl(
     private val context = containerView.context
     private val loginOption: MenuItem
     private val profileOption: MenuItem
-    private val darkThemeOption: MenuItem
 
     init {
         toolbar.inflateMenu(R.menu.options)
@@ -69,24 +72,32 @@ class MainViewImpl(
                     callbacks.onAboutOptionClicked()
                     true
                 }
-                R.id.option_dark_theme -> {
-                    item.isChecked = !item.isChecked
-                    callbacks.onDarkThemeOptionClicked(item.isChecked)
+                R.id.option_light_theme,
+                R.id.option_dark_theme,
+                R.id.option_default_theme -> {
+                    item.isChecked = true
+                    callbacks.onThemeOptionClicked(Theme.getById(item.itemId))
                     true
                 }
-                R.id.option_clear_cache -> {
+                R.id.debug_option_clear_cache -> {
                     context.cacheDir.deleteRecursively()
                     context.getSharedPreferences(COMMON_PREFS_NAME, MODE_PRIVATE)
                         .edit().clear().apply()
                     true
                 }
+                R.id.debug_option_toggle_theme -> {
+                    val newTheme = if (context.isNightMode) Theme.LIGHT else Theme.DARK
+                    callbacks.onThemeOptionClicked(newTheme)
+                    true
+                }
                 else -> false
             }
         }
         loginOption = toolbar.menu.findItem(R.id.option_login)
         profileOption = toolbar.menu.findItem(R.id.option_profile)
-        darkThemeOption = toolbar.menu.findItem(R.id.option_dark_theme)
-        toolbar.menu.findItem(R.id.option_clear_cache).isVisible = BuildConfig.DEBUG
+        DEBUG_OPTIONS.forEach { debugOption ->
+            toolbar.menu.findItem(debugOption).isVisible = BuildConfig.DEBUG
+        }
     }
 
     override fun showAboutDialog() {
@@ -133,8 +144,8 @@ class MainViewImpl(
         profileOption.isVisible = isVisible
     }
 
-    override fun setDarkThemeOptionChecked(isChecked: Boolean) {
-        darkThemeOption.isChecked = isChecked
+    override fun setThemeOptionChecked(theme: Theme) {
+        toolbar.menu.findItem(theme.id).isChecked = true
     }
 
 }

+ 0 - 3
app/src/main/java/com/sirekanyan/knigopis/feature/book/BookActivity.kt

@@ -5,8 +5,6 @@ import android.content.Intent
 import android.os.Bundle
 import com.sirekanyan.knigopis.R
 import com.sirekanyan.knigopis.common.BaseActivity
-import com.sirekanyan.knigopis.common.extensions.app
-import com.sirekanyan.knigopis.common.extensions.setDarkTheme
 import com.sirekanyan.knigopis.common.functions.extra
 import com.sirekanyan.knigopis.dependency.providePresenter
 import com.sirekanyan.knigopis.model.EditBookModel
@@ -21,7 +19,6 @@ class BookActivity : BaseActivity(), BookPresenter.Router {
     private val presenter by lazy { providePresenter(intent.getParcelableExtra(EXTRA_BOOK)!!) }
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        setDarkTheme(app.config.isDarkTheme)
         super.onCreate(savedInstanceState)
         setContentView(R.layout.book_edit)
         presenter.init()

+ 0 - 3
app/src/main/java/com/sirekanyan/knigopis/feature/login/LoginActivity.kt

@@ -4,7 +4,6 @@ import android.content.ActivityNotFoundException
 import android.content.Context
 import android.content.Intent
 import android.content.Intent.ACTION_VIEW
-import android.content.Intent.FLAG_ACTIVITY_NO_HISTORY
 import android.net.Uri
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
@@ -12,7 +11,6 @@ import androidx.browser.customtabs.CustomTabsIntent
 import androidx.core.content.ContextCompat
 import com.sirekanyan.knigopis.R
 import com.sirekanyan.knigopis.common.extensions.app
-import com.sirekanyan.knigopis.common.extensions.setDarkTheme
 import com.sirekanyan.knigopis.dependency.providePresenter
 import com.sirekanyan.knigopis.feature.startMainActivity
 
@@ -29,7 +27,6 @@ class LoginActivity : AppCompatActivity(), LoginPresenter.Router {
     private val auth by lazy { app.authRepository }
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        setDarkTheme(app.config.isDarkTheme)
         super.onCreate(savedInstanceState)
         setContentView(R.layout.login_activity)
         presenter.init()

+ 0 - 3
app/src/main/java/com/sirekanyan/knigopis/feature/user/UserActivity.kt

@@ -6,8 +6,6 @@ import android.content.Intent
 import android.os.Bundle
 import com.sirekanyan.knigopis.R
 import com.sirekanyan.knigopis.common.BaseActivity
-import com.sirekanyan.knigopis.common.extensions.app
-import com.sirekanyan.knigopis.common.extensions.setDarkTheme
 import com.sirekanyan.knigopis.common.extensions.systemClipboardManager
 import com.sirekanyan.knigopis.common.functions.extra
 import com.sirekanyan.knigopis.dependency.providePresenter
@@ -35,7 +33,6 @@ class UserActivity : BaseActivity(), UserPresenter.Router {
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        setDarkTheme(app.config.isDarkTheme)
         super.onCreate(savedInstanceState)
         setContentView(R.layout.user_activity)
         presenter.init()

+ 5 - 7
app/src/main/java/com/sirekanyan/knigopis/repository/Configuration.kt

@@ -2,20 +2,18 @@ package com.sirekanyan.knigopis.repository
 
 import android.app.Application
 import android.content.Context.MODE_PRIVATE
-import com.sirekanyan.knigopis.repository.config.BooleanPreference
-import com.sirekanyan.knigopis.repository.config.IntPreference
+import com.sirekanyan.knigopis.repository.config.enumPreference
+import com.sirekanyan.knigopis.repository.config.intPreference
 
 private const val PREFS_NAME = "config"
 
 interface Configuration {
-    var isDarkTheme: Boolean
+    var theme: Theme
     var sortingMode: Int
-    var shouldShowSettings: Boolean
 }
 
 class ConfigurationImpl(context: Application) : Configuration {
     internal val prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
-    override var isDarkTheme by BooleanPreference()
-    override var sortingMode by IntPreference()
-    override var shouldShowSettings by BooleanPreference()
+    override var theme by enumPreference(Theme.DEFAULT)
+    override var sortingMode by intPreference()
 }

+ 45 - 0
app/src/main/java/com/sirekanyan/knigopis/repository/Theme.kt

@@ -0,0 +1,45 @@
+package com.sirekanyan.knigopis.repository
+
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.Q
+import androidx.annotation.IdRes
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.appcompat.app.AppCompatDelegate.NightMode
+import com.sirekanyan.knigopis.R
+
+enum class Theme(@IdRes val id: Int, @NightMode val mode: Int) {
+
+    LIGHT(
+        R.id.option_light_theme,
+        AppCompatDelegate.MODE_NIGHT_NO
+    ),
+
+    DARK(
+        R.id.option_dark_theme,
+        AppCompatDelegate.MODE_NIGHT_YES
+    ),
+
+    DEFAULT(
+        R.id.option_default_theme,
+        if (SDK_INT >= Q) {
+            AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+        } else {
+            AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
+        }
+    );
+
+    fun setup() {
+        AppCompatDelegate.setDefaultNightMode(mode)
+    }
+
+    companion object {
+
+        fun getById(@IdRes id: Int): Theme =
+            values().find { it.id == id } ?: DEFAULT
+
+        fun getCurrent(): Theme =
+            values().find { it.mode == AppCompatDelegate.getDefaultNightMode() } ?: DEFAULT
+
+    }
+
+}

+ 24 - 9
app/src/main/java/com/sirekanyan/knigopis/repository/config/PropertyDelegate.kt

@@ -2,10 +2,11 @@ package com.sirekanyan.knigopis.repository.config
 
 import android.content.SharedPreferences
 import android.content.SharedPreferences.Editor
+import android.util.Log
 import com.sirekanyan.knigopis.repository.ConfigurationImpl
 import kotlin.reflect.KProperty
 
-abstract class AbstractPreference<T>(
+class PreferenceDelegate<T>(
     private val load: SharedPreferences.(key: String) -> T,
     private val save: Editor.(key: String, value: T) -> Editor
 ) {
@@ -18,12 +19,26 @@ abstract class AbstractPreference<T>(
 
 }
 
-class IntPreference : AbstractPreference<Int>(
-    { key -> getInt(key, 0) },
-    { key, value -> putInt(key, value) }
-)
+fun intPreference(): PreferenceDelegate<Int> =
+    PreferenceDelegate({ key -> getInt(key, 0) }, { key, value -> putInt(key, value) })
 
-class BooleanPreference : AbstractPreference<Boolean>(
-    { key -> getBoolean(key, false) },
-    { key, value -> putBoolean(key, value) }
-)
+@Suppress("Unused")
+fun booleanPreference(): PreferenceDelegate<Boolean> =
+    PreferenceDelegate({ key -> getBoolean(key, false) }, { key, value -> putBoolean(key, value) })
+
+inline fun <reified T : Enum<T>> enumPreference(default: T): PreferenceDelegate<T> =
+    PreferenceDelegate(
+        { key ->
+            getString(key, null)?.let { stringValue ->
+                try {
+                    enumValueOf<T>(stringValue)
+                } catch (exception: IllegalArgumentException) {
+                    Log.e("knigopis", "cannot deserialize enum", exception)
+                    null
+                }
+            } ?: default
+        },
+        { key, value ->
+            putString(key, value.toString())
+        }
+    )

+ 9 - 0
app/src/main/res/drawable/debug_ic_broom.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="?colorOnPrimary"
+        android:pathData="M19.36,2.72L20.78,4.14L15.06,9.85C16.13,11.39 16.28,13.24 15.38,14.44L9.06,8.12C10.26,7.22 12.11,7.37 13.65,8.44L19.36,2.72M5.93,17.57C3.92,15.56 2.69,13.16 2.35,10.92L7.23,8.83L14.67,16.27L12.58,21.15C10.34,20.81 7.94,19.58 5.93,17.57Z" />
+</vector>

+ 9 - 0
app/src/main/res/drawable/debug_ic_sunny.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="?colorOnPrimary"
+        android:pathData="M6.76,4.84l-1.8,-1.79 -1.41,1.41 1.79,1.79 1.42,-1.41zM4,10.5L1,10.5v2h3v-2zM13,0.55h-2L11,3.5h2L13,0.55zM20.45,4.46l-1.41,-1.41 -1.79,1.79 1.41,1.41 1.79,-1.79zM17.24,18.16l1.79,1.8 1.41,-1.41 -1.8,-1.79 -1.4,1.4zM20,10.5v2h3v-2h-3zM12,5.5c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6zM11,22.45h2L13,19.5h-2v2.95zM3.55,18.54l1.41,1.41 1.79,-1.8 -1.41,-1.41 -1.79,1.8z" />
+</vector>

+ 33 - 9
app/src/main/res/menu/options.xml

@@ -1,12 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <item
         android:id="@+id/option_login"
         android:title="@string/main.option.login"
         android:visible="false"
-        app:showAsAction="ifRoom" />
+        app:showAsAction="always"
+        tools:ignore="AlwaysShowAction" />
 
     <item
         android:id="@+id/option_profile"
@@ -14,21 +16,43 @@
         android:visible="false"
         app:showAsAction="never" />
 
+    <item android:title="@string/main_option_theme">
+        <menu>
+            <group android:checkableBehavior="single">
+                <item
+                    android:id="@+id/option_light_theme"
+                    android:title="@string/main_option_theme_light" />
+                <item
+                    android:id="@+id/option_dark_theme"
+                    android:title="@string/main_option_theme_dark" />
+                <item
+                    android:id="@+id/option_default_theme"
+                    android:title="@string/main_option_theme_default" />
+            </group>
+        </menu>
+    </item>
+
     <item
         android:id="@+id/option_about"
         android:title="@string/main.option.about"
         app:showAsAction="never" />
 
+    <!-- for debug only -->
     <item
-        android:id="@+id/option_dark_theme"
-        android:checkable="true"
-        android:title="@string/main.option.dark"
-        app:showAsAction="never" />
+        android:id="@+id/debug_option_clear_cache"
+        android:icon="@drawable/debug_ic_broom"
+        android:title="Clear cache"
+        android:visible="false"
+        app:showAsAction="always"
+        tools:ignore="HardcodedText" />
 
+    <!-- for debug only -->
     <item
-        android:id="@+id/option_clear_cache"
-        android:title="@string/main.option.clear"
+        android:id="@+id/debug_option_toggle_theme"
+        android:icon="@drawable/debug_ic_sunny"
+        android:title="Toggle theme"
         android:visible="false"
-        app:showAsAction="never" />
+        app:showAsAction="always"
+        tools:ignore="HardcodedText" />
 
 </menu>

+ 4 - 0
app/src/main/res/values-ru-v29/strings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="main_option_theme_default">Как в системе</string>
+</resources>

+ 9 - 2
app/src/main/res/values-ru/strings.xml

@@ -22,8 +22,15 @@
     <string name="main.option.login">Войти</string>
     <string name="main.option.profile">Мой профиль</string>
     <string name="main.option.about">О приложении</string>
-    <string name="main.option.dark">Тёмная тема</string>
-    <string name="main.option.clear">Очистить кэш</string>
+    <string name="main_option_sort">Сортировать</string>
+    <string name="main_option_sort_by_progress">По прогрессу</string>
+    <string name="main_option_sort_by_time">По свежести</string>
+    <string name="main_option_sort_by_title">По названию</string>
+    <string name="main_option_sort_by_author">По автору</string>
+    <string name="main_option_theme">Тема приложения</string>
+    <string name="main_option_theme_light">Светлая</string>
+    <string name="main_option_theme_dark">Тёмная</string>
+    <string name="main_option_theme_default">Энергосбережение</string>
 
     <!-- login -->
     <string name="login.title">Войти через…</string>

+ 4 - 0
app/src/main/res/values-v29/strings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="main_option_theme_default">System default</string>
+</resources>

+ 4 - 2
app/src/main/res/values/strings.xml

@@ -21,8 +21,10 @@
     <string name="main.option.login">Login</string>
     <string name="main.option.profile">Profile</string>
     <string name="main.option.about">About</string>
-    <string name="main.option.dark">Dark theme</string>
-    <string name="main.option.clear">Clear cache</string>
+    <string name="main_option_theme">Theme</string>
+    <string name="main_option_theme_light">Light</string>
+    <string name="main_option_theme_dark">Dark</string>
+    <string name="main_option_theme_default">Set by Battery Saver</string>
 
     <!-- login -->
     <string name="login.title">Log in with…</string>