Refactor chapter list provider and improve TTS functionality
Build Android APK / build-apk (push) Successful in 12m10s
Build Android AAB / build-aab (push) Successful in 19m35s

- Removed the constant chapterPageSize and refactored ChapterListQuery to use a simpler approach for fetching chapters.
- Updated the chapter list provider to handle fetching all chapters in a single request with pagination.
- Enhanced error handling for fetching chapters by resolving canonical IDs when necessary.
- Modified TTS functionality to ensure proper handling of Android fallback reading and improved error management.
- Added a new setting to enable/disable TTS on sentence tap.
- Updated UI components in the reader screen for better user experience and added navigation buttons for chapters.
- Bumped version to 1.0.3+4 in pubspec.yaml.
This commit is contained in:
2026-04-24 03:03:32 +07:00
parent 2b8fa4ee57
commit 66613857e8
11 changed files with 1112 additions and 447 deletions
@@ -7,6 +7,7 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
@@ -93,21 +94,27 @@ class ReaderTtsMediaService : Service(), TextToSpeech.OnInitListener {
language: String,
voiceName: String?,
backgroundModeEnabled: Boolean,
) {
ContextCompat.startForegroundService(
context,
Intent(context, ReaderTtsMediaService::class.java).apply {
action = ACTION_START_READING
putParcelableArrayListExtra(EXTRA_SEGMENTS, segments)
putExtra(EXTRA_START_INDEX, startIndex)
putExtra(EXTRA_CONTENT_KEY, contentKey)
putExtra(EXTRA_TITLE, title)
putExtra(EXTRA_SPEED, speed)
putExtra(EXTRA_LANGUAGE, language)
putExtra(EXTRA_VOICE_NAME, voiceName)
putExtra(EXTRA_BACKGROUND_MODE_ENABLED, backgroundModeEnabled)
},
)
): Boolean {
return try {
ContextCompat.startForegroundService(
context,
Intent(context, ReaderTtsMediaService::class.java).apply {
action = ACTION_START_READING
putParcelableArrayListExtra(EXTRA_SEGMENTS, segments)
putExtra(EXTRA_START_INDEX, startIndex)
putExtra(EXTRA_CONTENT_KEY, contentKey)
putExtra(EXTRA_TITLE, title)
putExtra(EXTRA_SPEED, speed)
putExtra(EXTRA_LANGUAGE, language)
putExtra(EXTRA_VOICE_NAME, voiceName)
putExtra(EXTRA_BACKGROUND_MODE_ENABLED, backgroundModeEnabled)
},
)
true
} catch (e: Throwable) {
Log.e(TAG, "startForegroundService blocked or failed", e)
false
}
}
fun pause(context: Context) =
@@ -905,7 +912,8 @@ class ReaderTtsMediaService : Service(), TextToSpeech.OnInitListener {
@SuppressLint("MissingPermission")
private fun buildNotification() = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
// Avoid adaptive launcher icon for foreground notifications on strict OEM ROMs.
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentTitle(title ?: appLabel())
.setContentText(currentProgressLabel())
.setContentIntent(buildLaunchIntent())
@@ -995,8 +1003,7 @@ class ReaderTtsMediaService : Service(), TextToSpeech.OnInitListener {
// to prevent Android from killing us.
if (status == "playing" || status == "paused") {
val notification = buildNotification()
startForeground(NOTIFICATION_ID, notification)
isForegroundActive = true
isForegroundActive = startForegroundCompat(notification)
}
return
}
@@ -1005,8 +1012,7 @@ class ReaderTtsMediaService : Service(), TextToSpeech.OnInitListener {
"playing", "paused" -> {
val notification = buildNotification()
if (!isForegroundActive) {
startForeground(NOTIFICATION_ID, notification)
isForegroundActive = true
isForegroundActive = startForegroundCompat(notification)
} else {
notificationManager.notify(NOTIFICATION_ID, notification)
}
@@ -1021,6 +1027,24 @@ class ReaderTtsMediaService : Service(), TextToSpeech.OnInitListener {
}
}
private fun startForegroundCompat(notification: android.app.Notification): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
true
} catch (e: Throwable) {
Log.e(TAG, "startForeground failed", e)
false
}
}
private fun publishSnapshot() {
val segment = currentSegment()
val canExposeSegmentProgress = status == "playing" && currentUtteranceStarted