Compare commits

...

9 Commits

Author SHA1 Message Date
Roshan Varughese
7c7af72f8c
Add option to opt out of Analytics and Crashlytics (#1237) 2024-10-12 06:46:28 +06:00
AntsyLich
c8bb78d91a
Tweak profile compilation status output
Co-authored-by: p
2024-10-12 06:23:37 +06:00
AntsyLich
2ba3f0612c
Remove usage of deprecated accompanist SystemUiController
Co-authored-by: p
2024-10-12 06:22:34 +06:00
AntsyLich
f84d9a08b4
ChapterNavigator: dispatch page change only when needed
Co-authored-by: p
2024-10-12 05:12:38 +06:00
AntsyLich
37419cdc26
Bump compile sdk to 35
Co-authored-by: p
2024-10-12 05:11:58 +06:00
AntsyLich
481cfedf08
Update resources exclusion rules
Co-authored-by: p
2024-10-12 05:11:16 +06:00
AntsyLich
9b8ab6acc2
Adjust distinct checker in WidgetManager and run on default dispatcher
Co-authored-by: p
2024-10-12 05:09:51 +06:00
AntsyLich
3bddb55385
Tweak Preference.collectAsState
Co-authored-by: p
2024-10-12 05:02:56 +06:00
AntsyLich
2beb89d531
Cleanup LibraryScreenModel LibraryMap.applySort and some more 2024-10-12 05:00:56 +06:00
23 changed files with 291 additions and 139 deletions

View File

@ -108,13 +108,16 @@ android {
packaging {
resources.excludes.addAll(
listOf(
"kotlin-tooling-metadata.json",
"META-INF/DEPENDENCIES",
"LICENSE.txt",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/**/LICENSE.txt",
"META-INF/*.properties",
"META-INF/**/*.properties",
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.kotlin_module",
"META-INF/*.version",
),
)
}
@ -161,7 +164,6 @@ dependencies {
debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
implementation(compose.accompanist.systemuicontroller)
implementation(androidx.interpolator)

View File

@ -0,0 +1,9 @@
package mihon.core.firebase
import android.content.Context
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import kotlinx.coroutines.CoroutineScope
object Firebase {
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) = Unit
}

View File

@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
withIOContext {
value = try {
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
val appResources = context.packageManager.getResourcesForApplication(appInfo)
Result.Success(
appResources.getDrawableForDensity(appInfo.icon, density, null)!!

View File

@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@ -34,13 +36,18 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.injectLazy
internal class PermissionStep : OnboardingStep {
private val privacyPreferences: PrivacyPreferences by injectLazy()
private var notificationGranted by mutableStateOf(false)
private var batteryGranted by mutableStateOf(false)
@ -73,7 +80,7 @@ internal class PermissionStep : OnboardingStep {
}
Column {
PermissionItem(
PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_install_apps),
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
granted = installGranted,
@ -89,7 +96,7 @@ internal class PermissionStep : OnboardingStep {
// no-op. resulting checks is being done on resume
},
)
PermissionItem(
PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_notifications),
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
granted = notificationGranted,
@ -97,7 +104,7 @@ internal class PermissionStep : OnboardingStep {
)
}
PermissionItem(
PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
granted = batteryGranted,
@ -109,6 +116,29 @@ internal class PermissionStep : OnboardingStep {
context.startActivity(intent)
},
)
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
val crashlyticsPref = privacyPreferences.crashlytics()
val crashlytics by crashlyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
granted = crashlytics,
onToggleChange = crashlyticsPref::set,
)
val analyticsPref = privacyPreferences.analytics()
val analytics by analyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
granted = analytics,
onToggleChange = analyticsPref::set,
)
}
}
@ -127,7 +157,7 @@ internal class PermissionStep : OnboardingStep {
}
@Composable
private fun PermissionItem(
private fun PermissionCheckbox(
title: String,
subtitle: String,
granted: Boolean,
@ -157,4 +187,26 @@ internal class PermissionStep : OnboardingStep {
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
@Composable
private fun PermissionSwitch(
title: String,
subtitle: String,
granted: Boolean,
modifier: Modifier = Modifier,
onToggleChange: (Boolean) -> Unit,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(text = title) },
supportingContent = { Text(text = subtitle) },
trailingContent = {
Switch(
checked = granted,
onCheckedChange = onToggleChange,
)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
}

View File

@ -7,6 +7,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
@ -28,55 +29,91 @@ object SettingsSecurityScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val context = LocalContext.current
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val authSupported = remember { context.isAuthenticationSupported() }
val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
return listOf(
getSecurityGroup(securityPreferences),
getFirebaseGroup(privacyPreferences),
)
}
@Composable
private fun getSecurityGroup(
securityPreferences: SecurityPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val authSupported = remember { context.isAuthenticationSupported() }
val useAuthPref = securityPreferences.useAuthenticator()
val useAuth by useAuthPref.collectAsState()
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = useAuthPref,
title = stringResource(MR.strings.lock_with_biometrics),
enabled = authSupported,
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_with_biometrics),
)
},
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.lockAppAfter(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
entries = LockAfterValues
.associateWith {
when (it) {
-1 -> stringResource(MR.strings.lock_never)
0 -> stringResource(MR.strings.lock_always)
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_security),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = useAuthPref,
title = stringResource(MR.strings.lock_with_biometrics),
enabled = authSupported,
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_with_biometrics),
)
},
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.lockAppAfter(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
entries = LockAfterValues
.associateWith {
when (it) {
-1 -> stringResource(MR.strings.lock_never)
0 -> stringResource(MR.strings.lock_always)
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
}
}
}
.toImmutableMap(),
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle),
)
},
.toImmutableMap(),
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle),
)
},
),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
)
}
@Composable
private fun getFirebaseGroup(
privacyPreferences: PrivacyPreferences,
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
),
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.firebase_summary)),
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
)
}
}

View File

@ -78,7 +78,7 @@ class DebugInfoScreen : Screen() {
val status by produceState(initialValue = "-") {
val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode
value = when (result) {
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed"
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE_INSTALLED -> "No profile installed"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
"Compiled non-matching"
@ -88,6 +88,7 @@ class DebugInfoScreen : Screen() {
-> "Error $result"
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION -> "Not supported"
ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION -> "Pending compilation"
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED -> "No profile embedded"
else -> "Unknown code $result"
}
}

View File

@ -114,7 +114,10 @@ fun ChapterNavigator(
valueRange = 1f..totalPages.toFloat(),
steps = totalPages - 2,
onValueChange = {
onSliderValueChange(it.roundToInt() - 1)
val new = it.roundToInt() - 1
if (new != currentPage) {
onSliderValueChange(new)
}
},
interactionSource = interactionSource,
)

View File

@ -55,7 +55,10 @@ interface AssistContentScreen {
}
@Composable
fun DefaultNavigatorScreenTransition(navigator: Navigator) {
fun DefaultNavigatorScreenTransition(
navigator: Navigator,
modifier: Modifier = Modifier,
) {
val slideDistance = rememberSlideDistance()
ScreenTransition(
navigator = navigator,
@ -65,6 +68,7 @@ fun DefaultNavigatorScreenTransition(navigator: Navigator) {
slideDistance = slideDistance,
)
},
modifier = modifier,
)
}

View File

@ -26,6 +26,7 @@ import eu.kanade.domain.DomainModule
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
@ -50,6 +51,7 @@ import kotlinx.coroutines.flow.onEach
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import mihon.core.firebase.Firebase
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import org.conscrypt.Conscrypt
@ -67,6 +69,7 @@ import java.security.Security
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
private val basePreferences: BasePreferences by injectLazy()
private val privacyPreferences: PrivacyPreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@ -93,6 +96,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
Firebase.setup(applicationContext, privacyPreferences, ProcessLifecycleOwner.get().lifecycleScope)
setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)

View File

@ -5,6 +5,7 @@ import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
@ -39,6 +40,9 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory {
SecurityPreferences(get())
}
addSingletonFactory {
PrivacyPreferences(get())
}
addSingletonFactory {
LibraryPreferences(get())
}

View File

@ -87,7 +87,7 @@ class ExtensionManager(
?: return null
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
.loadIcon(context.packageManager)
}
}

View File

@ -140,7 +140,7 @@ internal object ExtensionLoader {
val path = it.absolutePath
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
?.apply { applicationInfo.fixBasePaths(path) }
?.apply { applicationInfo!!.fixBasePaths(path) }
}
?.filter { isPackageAnExtension(it) }
?.map { ExtensionInfo(packageInfo = it, isShared = false) }
@ -191,7 +191,7 @@ internal object ExtensionLoader {
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
?.takeIf { isPackageAnExtension(it) }
?.let {
it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath)
it.applicationInfo!!.fixBasePaths(privateExtensionFile.absolutePath)
ExtensionInfo(
packageInfo = it,
isShared = false,
@ -226,7 +226,7 @@ internal object ExtensionLoader {
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
val pkgManager = context.packageManager
val pkgInfo = extensionInfo.packageInfo
val appInfo = pkgInfo.applicationInfo
val appInfo = pkgInfo.applicationInfo!!
val pkgName = pkgInfo.packageName
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
@ -365,7 +365,7 @@ internal object ExtensionLoader {
*/
private fun getSignatures(pkgInfo: PackageInfo): List<String>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val signingInfo = pkgInfo.signingInfo
val signingInfo = pkgInfo.signingInfo!!
if (signingInfo.hasMultipleSigners()) {
signingInfo.apkContentsSigners
} else {

View File

@ -72,7 +72,6 @@ import tachiyomi.domain.track.model.Track
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Collections
/**
* Typealias for the library manga, using the category as keys, and list of manga as values.
@ -107,16 +106,14 @@ class LibraryScreenModel(
getTracksPerManga.subscribe(),
getTrackingFilterFlow(),
downloadCache.changes,
) { searchQuery, library, tracks, trackingFiler, _ ->
) { searchQuery, library, tracks, trackingFilter, _ ->
library
.applyFilters(tracks, trackingFiler)
.applySort(tracks, trackingFiler.keys)
.applyFilters(tracks, trackingFilter)
.applySort(tracks, trackingFilter.keys)
.mapValues { (_, value) ->
if (searchQuery != null) {
// Filter query
value.filter { it.matches(searchQuery) }
} else {
// Don't do anything
value
}
}
@ -171,12 +168,9 @@ class LibraryScreenModel(
.launchIn(screenModelScope)
}
/**
* Applies library filters to the given map of manga.
*/
private suspend fun LibraryMap.applyFilters(
trackMap: Map<Long, List<Track>>,
trackingFiler: Map<Long, TriState>,
trackingFilter: Map<Long, TriState>,
): LibraryMap {
val prefs = getLibraryItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded
@ -188,10 +182,10 @@ class LibraryScreenModel(
val filterCompleted = prefs.filterCompleted
val filterIntervalCustom = prefs.filterIntervalCustom
val isNotLoggedInAnyTrack = trackingFiler.isEmpty()
val isNotLoggedInAnyTrack = trackingFilter.isEmpty()
val excludedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val excludedTracks = trackingFilter.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = trackingFilter.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (LibraryItem) -> Boolean = {
@ -249,17 +243,10 @@ class LibraryScreenModel(
filterFnTracking(it)
}
return this.mapValues { entry -> entry.value.fastFilter(filterFn) }
return mapValues { (_, value) -> value.fastFilter(filterFn) }
}
/**
* Applies library sorting to the given map of manga.
*/
private fun LibraryMap.applySort(
// Map<MangaId, List<Track>>
trackMap: Map<Long, List<Track>>,
loggedInTrackerIds: Set<Long>,
): LibraryMap {
private fun LibraryMap.applySort(trackMap: Map<Long, List<Track>>, loggedInTrackerIds: Set<Long>): LibraryMap {
val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase())
}
@ -278,9 +265,8 @@ class LibraryScreenModel(
}
}
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
val sort = keys.find { it.id == i1.libraryManga.category }!!.sort
when (sort.type) {
fun LibrarySort.comparator(): Comparator<LibraryItem> = Comparator { i1, i2 ->
when (this.type) {
LibrarySort.Type.Alphabetical -> {
sortAlphabetically(i1, i2)
}
@ -293,8 +279,8 @@ class LibraryScreenModel(
LibrarySort.Type.UnreadCount -> when {
// Ensure unread content comes first
i1.libraryManga.unreadCount == i2.libraryManga.unreadCount -> 0
i1.libraryManga.unreadCount == 0L -> if (sort.isAscending) 1 else -1
i2.libraryManga.unreadCount == 0L -> if (sort.isAscending) -1 else 1
i1.libraryManga.unreadCount == 0L -> if (this.isAscending) 1 else -1
i2.libraryManga.unreadCount == 0L -> if (this.isAscending) -1 else 1
else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount)
}
LibrarySort.Type.TotalChapters -> {
@ -317,14 +303,12 @@ class LibraryScreenModel(
}
}
return this.mapValues { entry ->
val comparator = if (keys.find { it.id == entry.key.id }!!.sort.isAscending) {
Comparator(sortFn)
} else {
Collections.reverseOrder(sortFn)
}
return mapValues { (key, value) ->
val comparator = key.sort.comparator()
.let { if (key.sort.isAscending) it else it.reversed() }
.thenComparator(sortAlphabetically)
entry.value.sortedWith(comparator.thenComparator(sortAlphabetically))
value.sortedWith(comparator)
}
}

View File

@ -10,20 +10,25 @@ import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -31,16 +36,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.animation.doOnEnd
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.util.Consumer
import androidx.core.view.WindowCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
@ -48,7 +53,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
@ -97,7 +101,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.injectLazy
import androidx.compose.ui.graphics.Color.Companion as ComposeColor
class MainActivity : BaseActivity() {
@ -132,17 +135,13 @@ class MainActivity : BaseActivity() {
return
}
// Draw edge-to-edge
// TODO: replace with ComponentActivity#enableEdgeToEdge
WindowCompat.setDecorFitsSystemWindows(window, false)
setComposeContent {
val context = LocalContext.current
val incognito by preferences.incognitoMode().collectAsState()
val downloadOnly by preferences.downloadedOnly().collectAsState()
val indexing by downloadCache.isInitializing.collectAsState()
// Set status bar color considering the top app state banner
val systemUiController = rememberSystemUiController()
val isSystemInDarkTheme = isSystemInDarkTheme()
val statusBarBackgroundColor = when {
indexing -> IndexingBannerBackgroundColor
@ -150,27 +149,13 @@ class MainActivity : BaseActivity() {
incognito -> IncognitoModeBannerBackgroundColor
else -> MaterialTheme.colorScheme.surface
}
LaunchedEffect(systemUiController, statusBarBackgroundColor) {
systemUiController.setStatusBarColor(
color = ComposeColor.Transparent,
darkIcons = statusBarBackgroundColor.luminance() > 0.5,
transformColorForLightContent = { ComposeColor.Black },
)
}
// Set navigation bar color
val context = LocalContext.current
val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
LaunchedEffect(systemUiController, isSystemInDarkTheme, navbarScrimColor) {
systemUiController.setNavigationBarColor(
color = if (context.isNavigationBarNeedsScrim()) {
navbarScrimColor.copy(alpha = 0.7f)
} else {
ComposeColor.Transparent
},
darkIcons = !isSystemInDarkTheme,
navigationBarContrastEnforced = false,
transformColorForLightContent = { ComposeColor.Black },
LaunchedEffect(isSystemInDarkTheme, statusBarBackgroundColor) {
// Draw edge-to-edge and set system bars color to transparent
val lightStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.BLACK)
val darkStyle = SystemBarStyle.dark(Color.TRANSPARENT)
enableEdgeToEdge(
statusBarStyle = if (statusBarBackgroundColor.luminance() > 0.5) lightStyle else darkStyle,
navigationBarStyle = if (isSystemInDarkTheme) darkStyle else lightStyle,
)
}
@ -203,13 +188,26 @@ class MainActivity : BaseActivity() {
contentWindowInsets = scaffoldInsets,
) { contentPadding ->
// Consume insets already used by app state banners
Box(
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding),
) {
Box {
// Shows current screen
DefaultNavigatorScreenTransition(navigator = navigator)
DefaultNavigatorScreenTransition(
navigator = navigator,
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding),
)
// Draw navigation bar scrim when needed
if (remember { isNavigationBarNeedsScrim() }) {
Spacer(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.windowInsetsBottomHeight(WindowInsets.navigationBars)
.alpha(0.8f)
.background(MaterialTheme.colorScheme.surfaceContainer),
)
}
}
}

View File

@ -20,6 +20,15 @@
tools:node="remove" />
<application>
<!-- Disable for manual opt-in -->
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- Disable unnecessary stuff from Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"

View File

@ -0,0 +1,20 @@
package mihon.core.firebase
import android.content.Context
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
object Firebase {
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) {
preference.analytics().changes().onEach { enabled ->
FirebaseAnalytics.getInstance(context).setAnalyticsCollectionEnabled(enabled)
}.launchIn(scope)
preference.crashlytics().changes().onEach { enabled ->
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled)
}.launchIn(scope)
}
}

View File

@ -4,7 +4,7 @@ import org.gradle.api.JavaVersion as GradleJavaVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget as KotlinJvmTarget
object AndroidConfig {
const val COMPILE_SDK = 34
const val COMPILE_SDK = 35
const val TARGET_SDK = 34
const val MIN_SDK = 26
const val NDK = "27.1.12297006"

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.core.security
import tachiyomi.core.common.preference.PreferenceStore
class PrivacyPreferences(
private val preferenceStore: PreferenceStore,
) {
fun crashlytics() = preferenceStore.getBoolean("crashlytics", true)
fun analytics() = preferenceStore.getBoolean("analytics", true)
}

View File

@ -36,7 +36,7 @@ object WebViewUtil {
fun getVersion(context: Context): String {
val webView = WebView.getCurrentWebViewPackage() ?: return "how did you get here?"
val pm = context.packageManager
val label = webView.applicationInfo.loadLabel(pm)
val label = webView.applicationInfo!!.loadLabel(pm)
val version = webView.versionName
return "$label $version"
}

View File

@ -1,6 +1,5 @@
[versions]
compose-bom = "2024.09.03"
accompanist = "0.36.0"
[libraries]
activity = "androidx.activity:activity-compose:1.9.2"
@ -17,5 +16,3 @@ material3-core = { module = "androidx.compose.material3:material3" }
material-icons = { module = "androidx.compose.material:material-icons-extended" }
glance = "androidx.glance:glance-appwidget:1.1.0"
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }

View File

@ -190,6 +190,10 @@
<string name="onboarding_permission_notifications_description">Get notified for library updates and more.</string>
<string name="onboarding_permission_ignore_battery_opts">Background battery usage</string>
<string name="onboarding_permission_ignore_battery_opts_description">Avoid interruptions to long-running library updates, downloads, and backup restores.</string>
<string name="onboarding_permission_crashlytics">Send crash logs</string>
<string name="onboarding_permission_crashlytics_description">Send anonymized crash logs to the developers.</string>
<string name="onboarding_permission_analytics">Allow analytics</string>
<string name="onboarding_permission_analytics_description">Send anonymized usage data to improve app features.</string>
<string name="onboarding_permission_action_grant">Grant</string>
<string name="onboarding_guides_new_user">New to %s? We recommend checking out the getting started guide.</string>
<string name="onboarding_guides_returning_user">Reinstalling %s?</string>
@ -242,6 +246,9 @@
<string name="pref_app_language">App language</string>
<string name="pref_category_security">Security and privacy</string>
<string name="pref_security">Security</string>
<string name="pref_firebase">Analytics and Crash logs</string>
<string name="lock_with_biometrics">Require unlock</string>
<string name="lock_when_idle">Lock when idle</string>
<string name="lock_always">Always</string>
@ -249,6 +256,7 @@
<string name="hide_notification_content">Hide notification content</string>
<string name="secure_screen">Secure screen</string>
<string name="secure_screen_summary">Secure screen hides app contents when switching apps and block screenshots</string>
<string name="firebase_summary">Sending crash logs and analytics will allow us to identify and fix issues, improve performance, and make future updates more relevant to your needs</string>
<string name="pref_category_nsfw_content">NSFW (18+) sources</string>
<string name="pref_show_nsfw_source">Show in sources and extensions lists</string>

View File

@ -4,10 +4,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.CoroutineScope
import tachiyomi.core.common.preference.Preference
@Composable
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
return flow.collectAsState(initial = get())
fun <T> Preference<T>.collectAsState(scope: CoroutineScope = rememberCoroutineScope()): State<T> {
val flow = remember(this) { stateIn(scope) }
return flow.collectAsState()
}

View File

@ -4,8 +4,10 @@ import android.content.Context
import androidx.glance.appwidget.updateAll
import androidx.lifecycle.LifecycleCoroutineScope
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
@ -21,9 +23,12 @@ class WidgetManager(
combine(
getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.toEpochMilli()),
securityPreferences.useAuthenticator().changes(),
transform = { a, _ -> a },
transform = { a, b -> a to b },
)
.distinctUntilChanged()
.distinctUntilChanged { old, new ->
old.second == new.second &&
old.first.map { it.chapterId }.toSet() == new.first.map { it.chapterId }.toSet()
}
.onEach {
try {
UpdatesGridGlanceWidget().updateAll(this)
@ -32,6 +37,7 @@ class WidgetManager(
logcat(LogPriority.ERROR, e) { "Failed to update widget" }
}
}
.flowOn(Dispatchers.Default)
.launchIn(scope)
}
}