Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac9a22dbe1 | ||
|
|
cf29f19511 | ||
|
|
ed1e1db7f2 | ||
|
|
a5b54def51 | ||
|
|
ee19f84f91 | ||
|
|
4e42a269a7 | ||
|
|
4caf3a534d | ||
|
|
c0c9211b82 | ||
|
|
f5880d72ad | ||
|
|
95f4317eaf | ||
|
|
df96deb1fc | ||
|
|
451267688a | ||
|
|
1d84eaf9c0 | ||
|
|
59115ff9a3 |
No files matched your search
@@ -109,7 +109,7 @@ object DictionaryProgressModel {
|
|||||||
return Snapshot(
|
return Snapshot(
|
||||||
progress = p,
|
progress = p,
|
||||||
percent = (p * 100f).toInt().coerceIn(0, 100),
|
percent = (p * 100f).toInt().coerceIn(0, 100),
|
||||||
titleRes = R.string.notification_indexing_title,
|
titleRes = R.string.notification_preparing_indexing_title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ object DictionaryProgressModel {
|
|||||||
} else {
|
} else {
|
||||||
DOWNLOAD_WEIGHT + EXTRACT_WEIGHT + idx * INDEX_WEIGHT
|
DOWNLOAD_WEIGHT + EXTRACT_WEIGHT + idx * INDEX_WEIGHT
|
||||||
}
|
}
|
||||||
val pct = (p * 100f).toInt().coerceIn(0, 100)
|
val pct = (p * 100f).toInt().coerceIn(0, 99)
|
||||||
|
|
||||||
return Snapshot(
|
return Snapshot(
|
||||||
progress = p,
|
progress = p,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class DictionaryProgressPresenter(
|
|||||||
|
|
||||||
fun beginFlow() {
|
fun beginFlow() {
|
||||||
terminalActive = false
|
terminalActive = false
|
||||||
|
notifications.cancelTerminalNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showImportSuccess() {
|
override fun showImportSuccess() {
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ interface NotificationPort {
|
|||||||
fun showDownloadSuccess()
|
fun showDownloadSuccess()
|
||||||
fun showError()
|
fun showError()
|
||||||
fun cancelProgressNotification()
|
fun cancelProgressNotification()
|
||||||
|
fun cancelTerminalNotification()
|
||||||
fun hasActiveProgressNotification(): Boolean
|
fun hasActiveProgressNotification(): Boolean
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ class NotificationHelper(private val context: Context) : NotificationPort {
|
|||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = "download_progress_channel"
|
const val CHANNEL_ID = "download_progress_channel"
|
||||||
const val NOTIFICATION_ID = 1
|
const val NOTIFICATION_ID = 1
|
||||||
|
const val TERMINAL_NOTIFICATION_ID = 2
|
||||||
}
|
}
|
||||||
private val notificationManager = NotificationManagerCompat.from(context)
|
private val notificationManager = NotificationManagerCompat.from(context)
|
||||||
private var lastNotificationKey: Int = 0
|
private var lastNotificationKey: Int = 0
|
||||||
@@ -91,7 +92,7 @@ class NotificationHelper(private val context: Context) : NotificationPort {
|
|||||||
.setContentIntent(mainActivityPendingIntent)
|
.setContentIntent(mainActivityPendingIntent)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
safeNotify(notification)
|
safeNotifyTerminal(notification)
|
||||||
}
|
}
|
||||||
fun showErrorNotification() {
|
fun showErrorNotification() {
|
||||||
if (!canShowNotification()) return
|
if (!canShowNotification()) return
|
||||||
@@ -103,13 +104,17 @@ class NotificationHelper(private val context: Context) : NotificationPort {
|
|||||||
.setContentIntent(mainActivityPendingIntent)
|
.setContentIntent(mainActivityPendingIntent)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
safeNotify(notification)
|
safeNotifyTerminal(notification)
|
||||||
}
|
}
|
||||||
fun cancelNotification() {
|
fun cancelNotification() {
|
||||||
lastNotificationKey = 0
|
lastNotificationKey = 0
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun cancelTerminalNotification() {
|
||||||
|
notificationManager.cancel(TERMINAL_NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
override fun hasActiveProgressNotification(): Boolean = lastNotificationKey != 0
|
override fun hasActiveProgressNotification(): Boolean = lastNotificationKey != 0
|
||||||
|
|
||||||
fun showUnifiedProgressNotification(
|
fun showUnifiedProgressNotification(
|
||||||
@@ -161,6 +166,14 @@ class NotificationHelper(private val context: Context) : NotificationPort {
|
|||||||
// Notification permission denied or security policy blocked
|
// Notification permission denied or security policy blocked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun safeNotifyTerminal(notification: Notification) {
|
||||||
|
try {
|
||||||
|
notificationManager.notify(TERMINAL_NOTIFICATION_ID, notification)
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
fun showImportSuccessNotification() {
|
fun showImportSuccessNotification() {
|
||||||
if (!canShowNotification()) return
|
if (!canShowNotification()) return
|
||||||
lastNotificationKey = 0
|
lastNotificationKey = 0
|
||||||
@@ -171,7 +184,7 @@ class NotificationHelper(private val context: Context) : NotificationPort {
|
|||||||
.setContentIntent(mainActivityPendingIntent)
|
.setContentIntent(mainActivityPendingIntent)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.build()
|
.build()
|
||||||
safeNotify(notification)
|
safeNotifyTerminal(notification)
|
||||||
}
|
}
|
||||||
fun createForegroundNotification(): Notification {
|
fun createForegroundNotification(): Notification {
|
||||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
|||||||
@@ -102,12 +102,15 @@ data class DictionarySource(
|
|||||||
|
|
||||||
fun validateTemplate(template: String): ValidationResult {
|
fun validateTemplate(template: String): ValidationResult {
|
||||||
val trimmed = template.trim()
|
val trimmed = template.trim()
|
||||||
|
val lowerTemplate = trimmed.lowercase()
|
||||||
return when {
|
return when {
|
||||||
trimmed.isBlank() -> ValidationResult.Error(ValidationError.EMPTY)
|
trimmed.isBlank() -> ValidationResult.Error(ValidationError.EMPTY)
|
||||||
trimmed.length > 2048 -> ValidationResult.Error(ValidationError.TOO_LONG)
|
trimmed.length > 2048 -> ValidationResult.Error(ValidationError.TOO_LONG)
|
||||||
!trimmed.startsWith("http://") && !trimmed.startsWith("https://") ->
|
!trimmed.startsWith("http://") && !trimmed.startsWith("https://") ->
|
||||||
ValidationResult.Error(ValidationError.INVALID_PROTOCOL)
|
ValidationResult.Error(ValidationError.INVALID_PROTOCOL)
|
||||||
!trimmed.endsWith(".gz") && !trimmed.endsWith(".dsl") && !trimmed.endsWith(".dz") ->
|
!lowerTemplate.endsWith(".gz") &&
|
||||||
|
!lowerTemplate.endsWith(".dsl") &&
|
||||||
|
!lowerTemplate.endsWith(".dsl.dz") ->
|
||||||
ValidationResult.Error(ValidationError.INVALID_EXTENSION)
|
ValidationResult.Error(ValidationError.INVALID_EXTENSION)
|
||||||
!isValidUrlStructure(trimmed) ->
|
!isValidUrlStructure(trimmed) ->
|
||||||
ValidationResult.Error(ValidationError.INVALID_URL_STRUCTURE)
|
ValidationResult.Error(ValidationError.INVALID_URL_STRUCTURE)
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class LocalDictionaryRepository(
|
|||||||
progressTracker = ProgressTracker(filesToScan)
|
progressTracker = ProgressTracker(filesToScan)
|
||||||
|
|
||||||
existingDictionaries.forEach { dict ->
|
existingDictionaries.forEach { dict ->
|
||||||
progressTracker?.updateFileProgress(dict.name, 1.0f)
|
progressTracker?.updateFileProgress(File(dict.path).name, 1.0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableIndexingProgress.emit(
|
mutableIndexingProgress.emit(
|
||||||
|
|||||||
+20
-4
@@ -64,7 +64,14 @@ class DictionaryChecker(
|
|||||||
val previousDate = DateUtils.getPreviousDateString()
|
val previousDate = DateUtils.getPreviousDateString()
|
||||||
val dictFiles = getDictFiles(folderUri, files)
|
val dictFiles = getDictFiles(folderUri, files)
|
||||||
|
|
||||||
val allHaveToday = enabledSources.all { source ->
|
val datedSources = enabledSources.filter { source ->
|
||||||
|
DictionarySource.hasDatePlaceholder(source.urlTemplate)
|
||||||
|
}
|
||||||
|
if (datedSources.isEmpty()) {
|
||||||
|
return@withContext true
|
||||||
|
}
|
||||||
|
|
||||||
|
val allHaveToday = datedSources.all { source ->
|
||||||
dictFiles.hasFileForSource(source, currentDate)
|
dictFiles.hasFileForSource(source, currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,19 +79,28 @@ class DictionaryChecker(
|
|||||||
return@withContext true
|
return@withContext true
|
||||||
}
|
}
|
||||||
|
|
||||||
val allHaveYesterday = enabledSources.all { source ->
|
val allHaveYesterday = datedSources.all { source ->
|
||||||
dictFiles.hasFileForSource(source, previousDate)
|
dictFiles.hasFileForSource(source, previousDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allHaveYesterday) {
|
if (allHaveYesterday) {
|
||||||
val serverHasToday = enabledSources.any { source ->
|
val serverHasToday = datedSources.any { source ->
|
||||||
isSourceAvailableOnServer(source, currentDate)
|
isSourceAvailableOnServer(source, currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withContext !serverHasToday
|
return@withContext !serverHasToday
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
val serverCanOfferNewer = datedSources.any { source ->
|
||||||
|
when (probeSource(source, currentDate)) {
|
||||||
|
SourceAvailability.AVAILABLE -> true
|
||||||
|
SourceAvailability.NOT_FOUND ->
|
||||||
|
probeSource(source, previousDate) == SourceAvailability.AVAILABLE
|
||||||
|
SourceAvailability.UNKNOWN -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext !serverCanOfferNewer
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error checking currency: ${e.message}", e)
|
Log.e(TAG, "Error checking currency: ${e.message}", e)
|
||||||
false
|
false
|
||||||
|
|||||||
+7
-1
@@ -88,6 +88,12 @@ class DictionaryForegroundService : Service() {
|
|||||||
stopForTimeout(startId)
|
stopForTimeout(startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
|
cancelAllWork()
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||||
stopForTimeout(startId)
|
stopForTimeout(startId)
|
||||||
}
|
}
|
||||||
@@ -125,7 +131,7 @@ class DictionaryForegroundService : Service() {
|
|||||||
startForegroundService()
|
startForegroundService()
|
||||||
}
|
}
|
||||||
ACTION_FINISH -> {
|
ACTION_FINISH -> {
|
||||||
stopForegroundService(removeNotification = false)
|
stopForegroundService(removeNotification = true)
|
||||||
}
|
}
|
||||||
ACTION_STOP -> {
|
ACTION_STOP -> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -123,59 +123,63 @@ fun ArticleContent(
|
|||||||
val isHeavyArticle =
|
val isHeavyArticle =
|
||||||
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
||||||
|
|
||||||
LazyColumn(
|
val articleList: @Composable (Modifier) -> Unit = { listModifier ->
|
||||||
state = listState,
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(top = Spacing.listItemVertical),
|
state = listState,
|
||||||
modifier = modifier
|
contentPadding = PaddingValues(top = Spacing.listItemVertical),
|
||||||
.fillMaxSize()
|
modifier = listModifier
|
||||||
.onSizeChanged { viewportSize = it }
|
.testTag("article_content")
|
||||||
.windowInsetsPadding(AppWindowInsets.horizontalSafeDrawing)
|
.semantics {
|
||||||
.padding(end = 16.dp)
|
testTagsAsResourceId = true
|
||||||
.testTag("article_content")
|
}
|
||||||
.semantics {
|
) {
|
||||||
testTagsAsResourceId = true
|
itemsIndexed(
|
||||||
}
|
items = articleState.dslContent,
|
||||||
) {
|
key = { index, _ ->
|
||||||
itemsIndexed(
|
// Use stable key combining article save key and index
|
||||||
items = articleState.dslContent,
|
"$articleSaveKey:$index"
|
||||||
key = { index, _ ->
|
},
|
||||||
// Use stable key combining article save key and index
|
contentType = { _, _ -> "article_block" }
|
||||||
"$articleSaveKey:$index"
|
) { index, block ->
|
||||||
},
|
val followedByContinuation =
|
||||||
contentType = { _, _ -> "article_block" }
|
articleState.dslContent.getOrNull(index + 1)?.continuesPrevious == true
|
||||||
) { index, block ->
|
val textContent = @Composable {
|
||||||
val followedByContinuation =
|
Box(
|
||||||
articleState.dslContent.getOrNull(index + 1)?.continuesPrevious == true
|
modifier = Modifier
|
||||||
val textContent = @Composable {
|
.padding(
|
||||||
Box(
|
start = Spacing.contentStart + indentStep * block.indent
|
||||||
modifier = Modifier
|
)
|
||||||
.padding(
|
.padding(bottom = if (followedByContinuation) 0.dp else 8.dp)
|
||||||
start = Spacing.contentStart + indentStep * block.indent
|
) {
|
||||||
|
ArticleLinkText(
|
||||||
|
block = block,
|
||||||
|
linksEnabled = true,
|
||||||
|
preferDirectHitTesting = isHeavyArticle,
|
||||||
|
style = baseTextStyle,
|
||||||
|
onRefClick = refClickHandler,
|
||||||
|
onTextLayout = {},
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("article_block_$index")
|
||||||
|
.semantics {
|
||||||
|
testTagsAsResourceId = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.padding(bottom = if (followedByContinuation) 0.dp else 8.dp)
|
}
|
||||||
) {
|
|
||||||
ArticleLinkText(
|
|
||||||
block = block,
|
|
||||||
linksEnabled = true,
|
|
||||||
preferDirectHitTesting = isHeavyArticle,
|
|
||||||
style = baseTextStyle,
|
|
||||||
onRefClick = refClickHandler,
|
|
||||||
onTextLayout = {},
|
|
||||||
modifier = Modifier
|
|
||||||
.testTag("article_block_$index")
|
|
||||||
.semantics {
|
|
||||||
testTagsAsResourceId = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (isHeavyArticle) {
|
|
||||||
textContent()
|
textContent()
|
||||||
} else {
|
|
||||||
SelectionContainer { textContent() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val articleListModifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.onSizeChanged { viewportSize = it }
|
||||||
|
.windowInsetsPadding(AppWindowInsets.horizontalSafeDrawing)
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
if (isHeavyArticle) {
|
||||||
|
articleList(articleListModifier)
|
||||||
|
} else {
|
||||||
|
SelectionContainer { articleList(articleListModifier) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is ArticleState.Error -> {
|
is ArticleState.Error -> {
|
||||||
Column(
|
Column(
|
||||||
@@ -207,7 +211,8 @@ fun ArticleTitleBar(
|
|||||||
articleState: ArticleState?,
|
articleState: ArticleState?,
|
||||||
showBackButton: Boolean,
|
showBackButton: Boolean,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
compact: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val title = when (articleState) {
|
val title = when (articleState) {
|
||||||
is ArticleState.Loading -> articleState.title
|
is ArticleState.Loading -> articleState.title
|
||||||
@@ -226,9 +231,14 @@ fun ArticleTitleBar(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.windowInsetsPadding(AppWindowInsets.topAndHorizontalSafeDrawing)
|
.windowInsetsPadding(AppWindowInsets.topAndHorizontalSafeDrawing)
|
||||||
.padding(
|
.then(
|
||||||
top = Spacing.listItemVertical,
|
if (compact) {
|
||||||
bottom = Spacing.listItemVertical
|
Modifier.padding(vertical = Spacing.listItemVertical)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.padding(top = Spacing.topBarPadding, bottom = Spacing.topBarPadding)
|
||||||
|
.height(Spacing.topBarHeight)
|
||||||
|
}
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -248,17 +258,19 @@ fun ArticleTitleBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
SelectionContainer {
|
||||||
text = title,
|
Text(
|
||||||
style = MaterialTheme.typography.dictionaryTitleLarge,
|
text = title,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
style = MaterialTheme.typography.dictionaryTitleLarge,
|
||||||
maxLines = 1,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
overflow = TextOverflow.Ellipsis,
|
maxLines = 1,
|
||||||
modifier = Modifier
|
overflow = TextOverflow.Ellipsis,
|
||||||
.padding(start = if (showBackButton) Spacing.searchTextStart - Spacing.iconStart - 24.dp else 16.dp)
|
modifier = Modifier
|
||||||
.testTag("article_title")
|
.padding(start = if (showBackButton) Spacing.searchTextStart - Spacing.iconStart - 24.dp else 16.dp)
|
||||||
.semantics { testTagsAsResourceId = true }
|
.testTag("article_title")
|
||||||
)
|
.semantics { testTagsAsResourceId = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,11 +70,6 @@ object DslAnnotatedParser {
|
|||||||
val endIndex: Int
|
val endIndex: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cache for applyLazyRefs results to avoid recreating AnnotatedString with links
|
|
||||||
// Size: 32 blocks - covers typical visible + prefetch range in LazyColumn
|
|
||||||
// Memory estimate: ~20KB per block average (AnnotatedString with LinkAnnotations)
|
|
||||||
// Total: ~640KB worst case
|
|
||||||
// LRU eviction automatic via LruCache
|
|
||||||
private val refCache = object : androidx.collection.LruCache<Pair<DslBlock, Int>, AnnotatedString>(32) {
|
private val refCache = object : androidx.collection.LruCache<Pair<DslBlock, Int>, AnnotatedString>(32) {
|
||||||
override fun sizeOf(key: Pair<DslBlock, Int>, value: AnnotatedString): Int = 1
|
override fun sizeOf(key: Pair<DslBlock, Int>, value: AnnotatedString): Int = 1
|
||||||
}
|
}
|
||||||
@@ -91,6 +86,7 @@ object DslAnnotatedParser {
|
|||||||
var accentNext = false
|
var accentNext = false
|
||||||
var currentIndent = 0
|
var currentIndent = 0
|
||||||
var mClosedPendingBreak = false
|
var mClosedPendingBreak = false
|
||||||
|
var lastAppended: Char? = null
|
||||||
val exampleStyle = SpanStyle(color = colorScheme.secondaryText)
|
val exampleStyle = SpanStyle(color = colorScheme.secondaryText)
|
||||||
val labelStyle = SpanStyle(color = colorScheme.labelText)
|
val labelStyle = SpanStyle(color = colorScheme.labelText)
|
||||||
val linkStyle = SpanStyle(color = colorScheme.linkText, textDecoration = TextDecoration.Underline)
|
val linkStyle = SpanStyle(color = colorScheme.linkText, textDecoration = TextDecoration.Underline)
|
||||||
@@ -108,20 +104,26 @@ object DslAnnotatedParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun flushBlock() {
|
fun flushBlock() {
|
||||||
if (builder.length > 0) {
|
val text = builder.toAnnotatedString()
|
||||||
|
if (text.any { !it.isWhitespace() }) {
|
||||||
result.add(DslBlock(
|
result.add(DslBlock(
|
||||||
text = builder.toAnnotatedString(),
|
text = text,
|
||||||
indent = currentIndent,
|
indent = currentIndent,
|
||||||
lazyRefs = lazyRefs.toList()
|
lazyRefs = lazyRefs.toList()
|
||||||
))
|
))
|
||||||
builder = AnnotatedString.Builder()
|
|
||||||
stack.forEach { builder.pushStyle(it) }
|
|
||||||
lazyRefs.clear()
|
|
||||||
}
|
}
|
||||||
|
builder = AnnotatedString.Builder()
|
||||||
|
stack.forEach { builder.pushStyle(it) }
|
||||||
|
lazyRefs.clear()
|
||||||
|
lastAppended = null
|
||||||
mClosedPendingBreak = false
|
mClosedPendingBreak = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun appendChar(char: Char) {
|
fun appendChar(char: Char, hard: Boolean = false) {
|
||||||
|
if (char == ' ' && !hard) {
|
||||||
|
if (lastAppended == null || lastAppended == ' ') return
|
||||||
|
}
|
||||||
|
lastAppended = char
|
||||||
if (mClosedPendingBreak) {
|
if (mClosedPendingBreak) {
|
||||||
mClosedPendingBreak = false
|
mClosedPendingBreak = false
|
||||||
if (refStack.isEmpty() && !char.isWhitespace()) {
|
if (refStack.isEmpty() && !char.isWhitespace()) {
|
||||||
@@ -134,6 +136,15 @@ object DslAnnotatedParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun appendText(value: String) {
|
||||||
|
if (value.isEmpty()) return
|
||||||
|
builder.append(value)
|
||||||
|
if (refStack.isNotEmpty()) {
|
||||||
|
refStack.last().second.append(value)
|
||||||
|
}
|
||||||
|
lastAppended = value.last()
|
||||||
|
}
|
||||||
|
|
||||||
fun marginIndent(tagName: String): Int {
|
fun marginIndent(tagName: String): Int {
|
||||||
if (tagName == "m" || tagName == "m0" || tagName == "m1") return 0
|
if (tagName == "m" || tagName == "m0" || tagName == "m1") return 0
|
||||||
val margin = tagName.drop(1).toIntOrNull() ?: return 0
|
val margin = tagName.drop(1).toIntOrNull() ?: return 0
|
||||||
@@ -205,12 +216,12 @@ object DslAnnotatedParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"p" -> {
|
"p" -> {
|
||||||
push(labelStyle)
|
push(labelStyle.copy(fontStyle = FontStyle.Italic))
|
||||||
}
|
}
|
||||||
"b" -> push(SpanStyle(fontWeight = FontWeight.Bold))
|
"b" -> push(SpanStyle(fontWeight = FontWeight.Bold))
|
||||||
"m", "m0", "m1", "m2", "m3", "m4", "m5" -> {
|
"m", "m0", "m1", "m2", "m3", "m4", "m5" -> {
|
||||||
if (builder.length > 0) {
|
if (lastAppended != null && !lastAppended!!.isWhitespace()) {
|
||||||
if (refStack.isEmpty()) flushBlock() else builder.append("\n")
|
flushBlock()
|
||||||
}
|
}
|
||||||
currentIndent = marginIndent(tagName)
|
currentIndent = marginIndent(tagName)
|
||||||
push(exampleStyle)
|
push(exampleStyle)
|
||||||
@@ -243,6 +254,18 @@ object DslAnnotatedParser {
|
|||||||
appendChar(char)
|
appendChar(char)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
'\n' -> {
|
||||||
|
if (refStack.isEmpty()) {
|
||||||
|
flushBlock()
|
||||||
|
} else {
|
||||||
|
appendChar(char)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
'\t' -> {
|
||||||
|
appendText("\u2002")
|
||||||
|
i++
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
appendChar(char)
|
appendChar(char)
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ fun AdaptiveMainScreen(
|
|||||||
articleState = articleState,
|
articleState = articleState,
|
||||||
showBackButton = isShowingDetail && articleState != null,
|
showBackButton = isShowingDetail && articleState != null,
|
||||||
onNavigateBack = onNavigateBackFromArticle,
|
onNavigateBack = onNavigateBackFromArticle,
|
||||||
|
compact = true,
|
||||||
modifier = Modifier.windowInsetsPadding(
|
modifier = Modifier.windowInsetsPadding(
|
||||||
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
|
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-1
@@ -1,7 +1,12 @@
|
|||||||
package com.example.research.ui.settings.components
|
package com.example.research.ui.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -115,7 +120,11 @@ fun DictionaryManagement(
|
|||||||
testTag = "dictionary_header"
|
testTag = "dictionary_header"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isExpanded) {
|
AnimatedVisibility(
|
||||||
|
visible = isExpanded,
|
||||||
|
enter = expandVertically(animationSpec = tween(250)) + fadeIn(tween(250)),
|
||||||
|
exit = shrinkVertically(animationSpec = tween(200)) + fadeOut(tween(200)),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
<string name="notification_download_error_title">Ошибка загрузки</string>
|
<string name="notification_download_error_title">Ошибка загрузки</string>
|
||||||
<string name="notification_download_error_text">Не удалось скачать словари</string>
|
<string name="notification_download_error_text">Не удалось скачать словари</string>
|
||||||
<string name="notification_indexing_title">Индексация словарей</string>
|
<string name="notification_indexing_title">Индексация словарей</string>
|
||||||
|
<string name="notification_preparing_indexing_title">Подготовка словарей к индексации</string>
|
||||||
<string name="archive_failed_open_source_file">Не удалось открыть исходный файл</string>
|
<string name="archive_failed_open_source_file">Не удалось открыть исходный файл</string>
|
||||||
<string name="archive_failed_create_output_file">Не удалось создать выходной файл</string>
|
<string name="archive_failed_create_output_file">Не удалось создать выходной файл</string>
|
||||||
<string name="archive_failed_delete_file">Не удалось удалить файл: %1$s</string>
|
<string name="archive_failed_delete_file">Не удалось удалить файл: %1$s</string>
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
<string name="dictionary_source_duplicate_prefix">Этот словарь уже существует (одинаковый префикс)</string>
|
<string name="dictionary_source_duplicate_prefix">Этот словарь уже существует (одинаковый префикс)</string>
|
||||||
<string name="dictionary_source_url_empty">URL не может быть пустым</string>
|
<string name="dictionary_source_url_empty">URL не может быть пустым</string>
|
||||||
<string name="dictionary_source_invalid_protocol">Должен начинаться с http:// или https://</string>
|
<string name="dictionary_source_invalid_protocol">Должен начинаться с http:// или https://</string>
|
||||||
<string name="dictionary_source_invalid_extension">Должен заканчиваться на .gz, .dsl или .dz</string>
|
<string name="dictionary_source_invalid_extension">Должен заканчиваться на .gz, .dsl или .dsl.dz</string>
|
||||||
<string name="dictionary_source_too_long">URL слишком длинный (максимум 2048 символов)</string>
|
<string name="dictionary_source_too_long">URL слишком длинный (максимум 2048 символов)</string>
|
||||||
<string name="dictionary_source_invalid_url">Неверный формат URL</string>
|
<string name="dictionary_source_invalid_url">Неверный формат URL</string>
|
||||||
<string name="dictionary_source_suspicious_characters">URL содержит недопустимые символы</string>
|
<string name="dictionary_source_suspicious_characters">URL содержит недопустимые символы</string>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
<string name="notification_download_error_title">Download error</string>
|
<string name="notification_download_error_title">Download error</string>
|
||||||
<string name="notification_download_error_text">Failed to download dictionaries</string>
|
<string name="notification_download_error_text">Failed to download dictionaries</string>
|
||||||
<string name="notification_indexing_title">Indexing dictionaries</string>
|
<string name="notification_indexing_title">Indexing dictionaries</string>
|
||||||
|
<string name="notification_preparing_indexing_title">Preparing dictionaries for indexing</string>
|
||||||
<string name="archive_failed_open_source_file">Failed to open source file</string>
|
<string name="archive_failed_open_source_file">Failed to open source file</string>
|
||||||
<string name="archive_failed_create_output_file">Failed to create output file</string>
|
<string name="archive_failed_create_output_file">Failed to create output file</string>
|
||||||
<string name="archive_failed_delete_file">Failed to delete file: %1$s</string>
|
<string name="archive_failed_delete_file">Failed to delete file: %1$s</string>
|
||||||
@@ -74,7 +75,7 @@
|
|||||||
<string name="dictionary_source_duplicate_prefix">This dictionary already exists</string>
|
<string name="dictionary_source_duplicate_prefix">This dictionary already exists</string>
|
||||||
<string name="dictionary_source_url_empty">URL cannot be empty</string>
|
<string name="dictionary_source_url_empty">URL cannot be empty</string>
|
||||||
<string name="dictionary_source_invalid_protocol">Must start with http:// or https://</string>
|
<string name="dictionary_source_invalid_protocol">Must start with http:// or https://</string>
|
||||||
<string name="dictionary_source_invalid_extension">Must end with .gz, .dsl, or .dz</string>
|
<string name="dictionary_source_invalid_extension">Must end with .gz, .dsl, or .dsl.dz</string>
|
||||||
<string name="dictionary_source_too_long">URL is too long (max 2048 characters)</string>
|
<string name="dictionary_source_too_long">URL is too long (max 2048 characters)</string>
|
||||||
<string name="dictionary_source_invalid_url">Invalid URL format</string>
|
<string name="dictionary_source_invalid_url">Invalid URL format</string>
|
||||||
<string name="dictionary_source_suspicious_characters">URL contains invalid characters</string>
|
<string name="dictionary_source_suspicious_characters">URL contains invalid characters</string>
|
||||||
|
|||||||
Reference in new issue
Block a user