Merge remote-tracking branch 'origin/main'

This commit is contained in:
Hosted Weblate 2026-05-10 06:48:53 +02:00
commit a79d379ff8
No known key found for this signature in database
GPG key ID: A3FAAA06E6569B4C
6 changed files with 77 additions and 2 deletions

View file

@ -124,4 +124,5 @@ data class ReaderScreenState(
val customAppThemes: List<CustomAppTheme> = emptyList(), val customAppThemes: List<CustomAppTheme> = emptyList(),
val allTags: List<TagEntity> = emptyList(), val allTags: List<TagEntity> = emptyList(),
val showTagSelectionDialogFor: Set<String> = emptySet(), val showTagSelectionDialogFor: Set<String> = emptySet(),
val isScreenCaptureProtectionEnabled: Boolean = false,
) )

View file

@ -359,6 +359,15 @@ fun HomeScreen(
onExportLogsClick = { viewModel.exportLogsToFile(context) }, onExportLogsClick = { viewModel.exportLogsToFile(context) },
onToggleHideReaderAi = { onToggleHideReaderAi = {
saveHideReaderAiFeatures(context, !loadHideReaderAiFeatures(context)) saveHideReaderAiFeatures(context, !loadHideReaderAiFeatures(context))
},
onScreenCaptureProtectionChange = { enabled ->
viewModel.setScreenCaptureProtectionEnabled(enabled)
val messageRes = if (enabled) {
R.string.banner_screen_capture_protection_on
} else {
R.string.banner_screen_capture_protection_off
}
viewModel.showBanner(context.getString(messageRes))
} }
) )
} else { } else {
@ -1019,7 +1028,8 @@ fun DefaultTopAppBar(
onTestSpeechBubbleDetectionClick: () -> Unit, onTestSpeechBubbleDetectionClick: () -> Unit,
onLanguageClick: () -> Unit, onLanguageClick: () -> Unit,
onExportLogsClick: () -> Unit, onExportLogsClick: () -> Unit,
onToggleHideReaderAi: () -> Unit onToggleHideReaderAi: () -> Unit,
onScreenCaptureProtectionChange: (Boolean) -> Unit
) { ) {
var showOptionsMenu by remember { mutableStateOf(false) } var showOptionsMenu by remember { mutableStateOf(false) }
var showLimitMenu by remember { mutableStateOf(false) } var showLimitMenu by remember { mutableStateOf(false) }
@ -1090,6 +1100,19 @@ fun DefaultTopAppBar(
} }
}) })
DropdownMenuItem(
text = { Text(stringResource(R.string.options_screen_capture_protection)) },
onClick = {
onScreenCaptureProtectionChange(!uiState.isScreenCaptureProtectionEnabled)
showOptionsMenu = false
},
trailingIcon = {
if (uiState.isScreenCaptureProtectionEnabled) {
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.content_desc_enabled))
}
}
)
DropdownMenuItem(text = { Text(stringResource(R.string.options_external_file_behavior)) }, onClick = { DropdownMenuItem(text = { Text(stringResource(R.string.options_external_file_behavior)) }, onClick = {
onExternalFileBehaviorClick() onExternalFileBehaviorClick()
showOptionsMenu = false showOptionsMenu = false

View file

@ -22,6 +22,7 @@ package com.aryan.reader
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import android.webkit.WebView import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -34,6 +35,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@ -87,6 +90,8 @@ class MainActivity : AppCompatActivity() {
setContent { setContent {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ScreenCaptureProtectionEffect(enabled = uiState.isScreenCaptureProtectionEnabled)
val darkTheme = when (uiState.appThemeMode) { val darkTheme = when (uiState.appThemeMode) {
AppThemeMode.LIGHT -> false AppThemeMode.LIGHT -> false
AppThemeMode.DARK -> true AppThemeMode.DARK -> true
@ -131,4 +136,21 @@ class MainActivity : AppCompatActivity() {
viewModel.onFileSelected(uri, isFromRecent = false, isExternalIntent = true) viewModel.onFileSelected(uri, isFromRecent = false, isExternalIntent = true)
} }
} }
} }
@Composable
private fun MainActivity.ScreenCaptureProtectionEffect(enabled: Boolean) {
DisposableEffect(enabled) {
if (enabled) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
onDispose {
if (enabled) {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
}

View file

@ -544,6 +544,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
activeTabBookId = prefs.getString(KEY_ACTIVE_TAB, null), activeTabBookId = prefs.getString(KEY_ACTIVE_TAB, null),
externalFileBehavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK", externalFileBehavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK",
useStrictFileFilter = prefs.getBoolean(KEY_USE_STRICT_FILE_FILTER, false), useStrictFileFilter = prefs.getBoolean(KEY_USE_STRICT_FILE_FILTER, false),
isScreenCaptureProtectionEnabled = prefs.getBoolean(KEY_SCREEN_CAPTURE_PROTECTION, false),
appThemeMode = try { appThemeMode = try {
AppThemeMode.valueOf(prefs.getString(KEY_APP_THEME_MODE, AppThemeMode.SYSTEM.name) ?: AppThemeMode.SYSTEM.name) AppThemeMode.valueOf(prefs.getString(KEY_APP_THEME_MODE, AppThemeMode.SYSTEM.name) ?: AppThemeMode.SYSTEM.name)
} catch (_: Exception) { AppThemeMode.SYSTEM }, } catch (_: Exception) { AppThemeMode.SYSTEM },
@ -5227,6 +5228,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { it.copy(useStrictFileFilter = enabled) } _internalState.update { it.copy(useStrictFileFilter = enabled) }
} }
fun setScreenCaptureProtectionEnabled(enabled: Boolean) {
prefs.edit { putBoolean(KEY_SCREEN_CAPTURE_PROTECTION, enabled) }
_internalState.update { it.copy(isScreenCaptureProtectionEnabled = enabled) }
}
private fun loadCustomAppThemes(prefs: SharedPreferences): List<CustomAppTheme> { private fun loadCustomAppThemes(prefs: SharedPreferences): List<CustomAppTheme> {
val jsonString = prefs.getString(KEY_CUSTOM_APP_THEMES, "[]") ?: "[]" val jsonString = prefs.getString(KEY_CUSTOM_APP_THEMES, "[]") ?: "[]"
val themes = mutableListOf<CustomAppTheme>() val themes = mutableListOf<CustomAppTheme>()
@ -5506,6 +5512,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private const val KEY_LAST_OPEN_FILE_TYPE = "last_open_file_type" private const val KEY_LAST_OPEN_FILE_TYPE = "last_open_file_type"
private const val KEY_EXTERNAL_FILE_BEHAVIOR = "external_file_behavior" private const val KEY_EXTERNAL_FILE_BEHAVIOR = "external_file_behavior"
private const val KEY_USE_STRICT_FILE_FILTER = "use_strict_file_filter" private const val KEY_USE_STRICT_FILE_FILTER = "use_strict_file_filter"
private const val KEY_SCREEN_CAPTURE_PROTECTION = "screen_capture_protection_enabled"
private const val KEY_APP_THEME_MODE = "app_theme_mode" private const val KEY_APP_THEME_MODE = "app_theme_mode"
private const val KEY_APP_CONTRAST_OPTION = "app_contrast_option" private const val KEY_APP_CONTRAST_OPTION = "app_contrast_option"
private const val KEY_APP_SEED_COLOR = "app_seed_color" private const val KEY_APP_SEED_COLOR = "app_seed_color"

View file

@ -1304,4 +1304,7 @@
<string name="action_exit_smart_zoom">Exit Smart Zoom</string> <string name="action_exit_smart_zoom">Exit Smart Zoom</string>
<string name="action_smart_comic_zoom">Smart Comic Zoom</string> <string name="action_smart_comic_zoom">Smart Comic Zoom</string>
<string name="desc_toggle_smart_comic_zoom">Toggle Smart Comic Zoom</string> <string name="desc_toggle_smart_comic_zoom">Toggle Smart Comic Zoom</string>
<string name="options_screen_capture_protection">Screen capture protection</string>
<string name="banner_screen_capture_protection_on">Screen capture protection is on</string>
<string name="banner_screen_capture_protection_off">Screen capture protection is off</string>
</resources> </resources>

View file

@ -319,6 +319,25 @@ class MainViewModelTest {
verify { mockEditor.putString("external_file_behavior", "KEEP") } verify { mockEditor.putString("external_file_behavior", "KEEP") }
} }
@Test
fun `screen capture protection persists and updates state`() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.uiState.collect {}
}
viewModel.setScreenCaptureProtectionEnabled(true)
val enabledState = viewModel.uiState.first { it.isScreenCaptureProtectionEnabled }
assertTrue(enabledState.isScreenCaptureProtectionEnabled)
verify { mockEditor.putBoolean("screen_capture_protection_enabled", true) }
viewModel.setScreenCaptureProtectionEnabled(false)
val disabledState = viewModel.uiState.first { !it.isScreenCaptureProtectionEnabled }
assertFalse(disabledState.isScreenCaptureProtectionEnabled)
verify { mockEditor.putBoolean("screen_capture_protection_enabled", false) }
}
@Test @Test
fun `setSortOrder persists preference and reorders visible home and library lists`() = runTest { fun `setSortOrder persists preference and reorders visible home and library lists`() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {