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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
android:launchMode="singleTop"
android:taskAffinity="moe.apex.breadboard.MainActivity"
android:exported="true"
android:theme="@style/Theme.Breadboard">
android:theme="@style/Theme.Breadboard"
android:configChanges="uiMode"> <!-- Add "uiMode" to "configChanges" to prevent the
activity from recreating itself during night mode
change, avoiding unnecessary state resets wherever
possible. -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand All @@ -29,7 +33,8 @@
android:taskAffinity="moe.apex.breadboard.DeepLinkActivity"
android:launchMode="singleTop"
android:exported="true"
android:theme="@style/Theme.Breadboard">
android:theme="@style/Theme.Breadboard"
android:configChanges="uiMode">

<intent-filter>
<action android:name="android.intent.action.VIEW" />
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/java/moe/apex/breadboard/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package moe.apex.breadboard

import android.app.UiModeManager
import android.content.Context
import android.content.Intent
import android.os.Build
Expand Down Expand Up @@ -35,6 +36,7 @@ import moe.apex.breadboard.navigation.Home
import moe.apex.breadboard.navigation.Navigation
import moe.apex.breadboard.navigation.Results
import moe.apex.breadboard.navigation.Search
import moe.apex.breadboard.preferences.DarkTheme
import moe.apex.breadboard.preferences.ImageSource
import moe.apex.breadboard.preferences.LocalPreferences
import moe.apex.breadboard.preferences.StartDestination
Expand Down Expand Up @@ -123,6 +125,25 @@ class MainActivity : SingletonImageLoader.Factory, ComponentActivity(), VolumeBu
val viewModel = getGlobalViewModel()
val recommendationsProvider by viewModel.recommendationsProvider.collectAsState()

/* Sync the UiModeManager's app night mode preference with the selected app dark theme
preference. This ensures that the splash screen colour scheme matches the selected app
dark theme preference.

UiModeManager#setApplicationNightMode() is only supported on Android 12+. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager

SideEffect(prefs.darkTheme) {
uiModeManager.setApplicationNightMode(
when (prefs.darkTheme) {
DarkTheme.ON -> UiModeManager.MODE_NIGHT_YES
DarkTheme.OFF -> UiModeManager.MODE_NIGHT_NO
DarkTheme.AUTO -> UiModeManager.MODE_NIGHT_AUTO
}
)
}
}

SideEffect(prefs.imageSource, prefs.filterRatingsLocally) {
if (
recommendationsProvider?.imageSource != prefs.imageSource ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package moe.apex.breadboard.home

import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -42,6 +41,7 @@ import moe.apex.breadboard.preferences.ImageSource
import moe.apex.breadboard.preferences.LocalPreferences
import moe.apex.breadboard.preferences.PreferenceKeys
import moe.apex.breadboard.prefs
import moe.apex.breadboard.ui.theme.shouldUseDarkTheme
import moe.apex.breadboard.util.DISABLED_OPACITY
import moe.apex.breadboard.util.ExpressiveTagEntryContainer
import moe.apex.breadboard.util.LargeTitleBar
Expand All @@ -65,7 +65,7 @@ fun FollowedArtistsScreen(navController: NavHostController) {
val userPreferencesRepository = LocalContext.current.prefs
val prefs = LocalPreferences.current
val followedTags = prefs.followedTags.toList().sorted()
val darkTheme = isSystemInDarkTheme()
val darkTheme = shouldUseDarkTheme()

MainScreenScaffold(
topAppBar = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import kotlinx.coroutines.launch
import moe.apex.breadboard.prefs
import moe.apex.breadboard.ui.theme.doesSystemSupportDarkTheme
import moe.apex.breadboard.util.LargeTitleBar
import moe.apex.breadboard.util.LazyExpressiveGroup
import moe.apex.breadboard.util.MainScreenScaffold
Expand Down Expand Up @@ -119,7 +120,26 @@ fun LayoutSettingsScreen(navController: NavHostController) {
}
}

LazyExpressiveGroup("Layout") {
LazyExpressiveGroup("Appearance and Layout") {
item {
EnumPref(
title = "Dark theme",
summary = currentSettings.darkTheme.label,
enumItems = DarkTheme.entries.filter { entry ->
// Hide the auto option if system dark theme is not supported
!(entry == DarkTheme.AUTO && !doesSystemSupportDarkTheme())
},
selectedItem = currentSettings.darkTheme,
onSelection = {
scope.launch {
preferencesRepository.updatePref(
PreferenceKeys.DARK_THEME,
it
)
}
}
)
}
item {
ReorderablePref(
title = "Reorder image actions",
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/moe/apex/breadboard/preferences/Pref.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import moe.apex.breadboard.image.Safebooru
import moe.apex.breadboard.image.Yandere
import moe.apex.breadboard.image.AI_TAG_NAMES
import moe.apex.breadboard.tag.TagCategory
import moe.apex.breadboard.ui.theme.doesSystemSupportDarkTheme
import moe.apex.breadboard.util.AgeVerification
import moe.apex.breadboard.util.MigrationOnlyField
import moe.apex.breadboard.util.PixivArtwork
Expand Down Expand Up @@ -98,6 +99,7 @@ data object PrefNames {
const val INTERNAL_IGNORE_LIST = "internal_ignore_list"
const val AUTOPLAY_VIDEOS = "autoplay_videos"
const val UNIFIED_INFO_SHEET = "unified_info_sheet"
const val DARK_THEME = "dark_theme"
const val FOLLOWED_TAGS = "followed_tags"
const val DEFAULT_BROWSE_TAB = "default_browse_tab"
const val PROFILES_FOR_ALL_TAGS = "profiles_for_all_tags"
Expand Down Expand Up @@ -136,6 +138,7 @@ object PreferenceKeys {
val INTERNAL_IGNORE_LIST = stringSetPreferencesKey(PrefNames.INTERNAL_IGNORE_LIST)
val AUTOPLAY_VIDEOS = stringPreferencesKey(PrefNames.AUTOPLAY_VIDEOS)
val UNIFIED_INFO_SHEET = booleanPreferencesKey(PrefNames.UNIFIED_INFO_SHEET)
val DARK_THEME = stringPreferencesKey(PrefNames.DARK_THEME)
val FOLLOWED_TAGS = stringSetPreferencesKey(PrefNames.FOLLOWED_TAGS)
val DEFAULT_BROWSE_TAB = stringPreferencesKey(PrefNames.DEFAULT_BROWSE_TAB)
val PROFILES_FOR_ALL_TAGS = booleanPreferencesKey(PrefNames.PROFILES_FOR_ALL_TAGS)
Expand Down Expand Up @@ -243,6 +246,7 @@ data class Prefs(
val internalIgnoreList: Set<String>,
val autoplayVideos: AutoplayVideosMode,
val unifiedInfoSheet: Boolean,
val darkTheme: DarkTheme,
val followedTags: Set<String>,
val defaultBrowseTab: BrowseTab,
val profilesForAllTags: Boolean,
Expand Down Expand Up @@ -280,6 +284,7 @@ data class Prefs(
internalIgnoreList = emptySet(),
autoplayVideos = AutoplayVideosMode.OFF,
unifiedInfoSheet = false, // Unified is called 'Classic' in the UI
darkTheme = DarkTheme.AUTO,
followedTags = emptySet(),
defaultBrowseTab = BrowseTab.FOR_YOU,
profilesForAllTags = false,
Expand Down Expand Up @@ -350,6 +355,7 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
PreferenceKeys.INTERNAL_IGNORE_LIST_TIMESTAMP to PrefMeta(PrefCategory.SETTING, exportable = false),
PreferenceKeys.INTERNAL_IGNORE_LIST to PrefMeta(PrefCategory.SETTING, exportable = false),
PreferenceKeys.UNIFIED_INFO_SHEET to PrefMeta(PrefCategory.SETTING),
PreferenceKeys.DARK_THEME to PrefMeta(PrefCategory.SETTING),
PreferenceKeys.FOLLOWED_TAGS to PrefMeta(PrefCategory.SETTING, mergeable = true),
PreferenceKeys.DEFAULT_BROWSE_TAB to PrefMeta(PrefCategory.SETTING),
PreferenceKeys.PROFILES_FOR_ALL_TAGS to PrefMeta(PrefCategory.SETTING),
Expand Down Expand Up @@ -393,6 +399,18 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
val currentPreferences = dataStore.data.first()
val lastUsedVersionCode = currentPreferences[PreferenceKeys.LAST_USED_VERSION_CODE] ?: 0

/* Version 3.3.2 introduces the manual dark theme preference. Android 9 had some partial
support for system dark theme, but older versions would have no support whatsoever.
Set the default dark theme preference to a manual OFF state on those versions. */
val darkTheme = currentPreferences[PreferenceKeys.DARK_THEME]

if (
!doesSystemSupportDarkTheme() &&
(darkTheme == null || darkTheme == DarkTheme.AUTO.name)
) {
updatePref(PreferenceKeys.DARK_THEME, DarkTheme.OFF)
}

/* lastUsedVersionCode can be 0 if the user had it installed already but cleared the data.
In such a case, we can't reliably determine what their previous version was. Just load
the default settings. */
Expand Down Expand Up @@ -858,6 +876,7 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
val internalIgnoreList = preferences[PreferenceKeys.INTERNAL_IGNORE_LIST] ?: Prefs.DEFAULT.internalIgnoreList
val autoplayVideos = preferences[PreferenceKeys.AUTOPLAY_VIDEOS]?.let { AutoplayVideosMode.valueOf(it) } ?: Prefs.DEFAULT.autoplayVideos
val unifiedInfoSheet = preferences[PreferenceKeys.UNIFIED_INFO_SHEET] ?: Prefs.DEFAULT.unifiedInfoSheet
val darkTheme = preferences[PreferenceKeys.DARK_THEME]?.let { DarkTheme.valueOf(it) } ?: Prefs.DEFAULT.darkTheme
val followedTags = preferences[PreferenceKeys.FOLLOWED_TAGS] ?: Prefs.DEFAULT.followedTags
val defaultBrowseTab = preferences[PreferenceKeys.DEFAULT_BROWSE_TAB]?.let { BrowseTab.valueOf(it) } ?: Prefs.DEFAULT.defaultBrowseTab
val profilesForAllTags = preferences[PreferenceKeys.PROFILES_FOR_ALL_TAGS] ?: Prefs.DEFAULT.profilesForAllTags
Expand Down Expand Up @@ -894,6 +913,7 @@ class UserPreferencesRepository(private val dataStore: DataStore<Preferences>) {
internalIgnoreList,
autoplayVideos,
unifiedInfoSheet,
darkTheme,
followedTags,
defaultBrowseTab,
profilesForAllTags,
Expand All @@ -913,3 +933,10 @@ enum class ImageSource(override val label: String, val imageBoard: ImageBoard) :
YANDERE("Yande.re", Yandere),
R34("Rule34", Rule34)
}


enum class DarkTheme(override val label: String) : PrefEnum<DarkTheme> {
ON("Always"),
OFF("Never"),
AUTO("Follow system")
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package moe.apex.breadboard.preferences

import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
Expand Down Expand Up @@ -47,6 +46,7 @@ import moe.apex.breadboard.navigation.DataSettings
import moe.apex.breadboard.navigation.ExperimentalSettings
import moe.apex.breadboard.navigation.GeneralSettings
import moe.apex.breadboard.navigation.LayoutSettings
import moe.apex.breadboard.ui.theme.shouldUseDarkTheme
import moe.apex.breadboard.util.ChevronRight
import moe.apex.breadboard.util.LazyExpressiveGroup
import moe.apex.breadboard.util.MainScreenScaffold
Expand Down Expand Up @@ -194,7 +194,7 @@ private fun ContainedIcon(
label: String,
imageVector: ImageVector
) {
val darkTheme = isSystemInDarkTheme()
val darkTheme = shouldUseDarkTheme()
val colourPair = remember(label) {
generateColours(darkTheme, label)
}
Expand Down
53 changes: 52 additions & 1 deletion app/src/main/java/moe/apex/breadboard/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package moe.apex.breadboard.ui.theme

import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.LocalActivity
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
Expand All @@ -11,9 +15,12 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import moe.apex.breadboard.preferences.DarkTheme
import moe.apex.breadboard.preferences.LocalPreferences


private val DarkColorScheme = darkColorScheme(
Expand Down Expand Up @@ -54,14 +61,58 @@ val LocalBreadboardColors = staticCompositionLocalOf {
}


@Composable
fun shouldUseDarkTheme(): Boolean {
val preferences = LocalPreferences.current

return when (preferences.darkTheme) {
DarkTheme.ON -> true
DarkTheme.OFF -> false
DarkTheme.AUTO -> isSystemInDarkTheme()
}
}


fun doesSystemSupportDarkTheme(): Boolean {
/* Android 9 had some partial support for system dark theme,
but older versions would have no support whatsoever. */
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
}


@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BreadboardTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
darkTheme: Boolean = shouldUseDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
/* Ensure that the status bar and navigation bar colour scheme match the current app dark theme
preference. This is necessary on Android 11 and lower if the app dark theme preference
is the opposite of the system dark theme preference.

This is not necessary for Android 12+ since we already sync the UiModeManager's app night mode
Comment thread
apex2504 marked this conversation as resolved.
preference to match the app dark theme preference, so the system changes the style accordingly. */
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
val context = LocalActivity.current as ComponentActivity

SideEffect(darkTheme) {
val systemBarStyle = if (darkTheme) {
SystemBarStyle.dark(
scrim = android.graphics.Color.TRANSPARENT
)
} else {
SystemBarStyle.light(
scrim = android.graphics.Color.TRANSPARENT,
darkScrim = android.graphics.Color.TRANSPARENT
)
}

context.enableEdgeToEdge(statusBarStyle = systemBarStyle, navigationBarStyle = systemBarStyle)
}
}

var colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
Expand Down
Loading