mirror of
https://github.com/mihonapp/mihon.git
synced 2025-03-27 14:55:29 +01:00
Compare commits
9 Commits
016f627fb0
...
7c7af72f8c
Author | SHA1 | Date | |
---|---|---|---|
|
7c7af72f8c | ||
|
c8bb78d91a | ||
|
2ba3f0612c | ||
|
f84d9a08b4 | ||
|
37419cdc26 | ||
|
481cfedf08 | ||
|
9b8ab6acc2 | ||
|
3bddb55385 | ||
|
2beb89d531 |
@ -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)
|
||||
|
||||
|
9
app/src/dev/java/mihon/core/firebase/Firebase.kt
Normal file
9
app/src/dev/java/mihon/core/firebase/Firebase.kt
Normal 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
|
||||
}
|
@ -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)!!
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
20
app/src/standard/java/mihon/core/firebase/Firebase.kt
Normal file
20
app/src/standard/java/mihon/core/firebase/Firebase.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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" }
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user