More SQLDelight migrations

This commit is contained in:
arkon 2022-07-02 15:19:52 -04:00
parent 5d77ee37d2
commit 21771e62aa
13 changed files with 63 additions and 57 deletions

View File

@ -26,6 +26,10 @@ class MangaRepositoryImpl(
return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) } return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) }
} }
override suspend fun getFavorites(): List<Manga> {
return handler.awaitList { mangasQueries.getFavorites(mangaMapper) }
}
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> { override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) } return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
} }

View File

@ -31,7 +31,7 @@ import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.interactor.UpsertHistory import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.repository.HistoryRepository import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.domain.manga.interactor.GetMangaById import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.interactor.GetMangaWithChapters import eu.kanade.domain.manga.interactor.GetMangaWithChapters
import eu.kanade.domain.manga.interactor.ResetViewerFlags import eu.kanade.domain.manga.interactor.ResetViewerFlags
@ -70,7 +70,7 @@ class DomainModule : InjektModule {
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) } addFactory { GetDuplicateLibraryManga(get()) }
addFactory { GetFavoritesBySourceId(get()) } addFactory { GetFavorites(get()) }
addFactory { GetMangaWithChapters(get(), get()) } addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaById(get()) } addFactory { GetMangaById(get()) }
addFactory { GetNextChapter(get()) } addFactory { GetNextChapter(get()) }

View File

@ -4,10 +4,14 @@ import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.domain.manga.repository.MangaRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class GetFavoritesBySourceId( class GetFavorites(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
) { ) {
suspend fun await(): List<Manga> {
return mangaRepository.getFavorites()
}
fun subscribe(sourceId: Long): Flow<List<Manga>> { fun subscribe(sourceId: Long): Flow<List<Manga>> {
return mangaRepository.getFavoritesBySourceId(sourceId) return mangaRepository.getFavoritesBySourceId(sourceId)
} }

View File

@ -31,9 +31,6 @@ data class Manga(
val initialized: Boolean, val initialized: Boolean,
) { ) {
val sorting: Long
get() = chapterFlags and CHAPTER_SORTING_MASK
fun toSManga(): SManga { fun toSManga(): SManga {
return SManga.create().also { return SManga.create().also {
it.url = url it.url = url
@ -48,6 +45,9 @@ data class Manga(
} }
} }
val sorting: Long
get() = chapterFlags and CHAPTER_SORTING_MASK
val displayMode: Long val displayMode: Long
get() = chapterFlags and CHAPTER_DISPLAY_MASK get() = chapterFlags and CHAPTER_DISPLAY_MASK

View File

@ -12,6 +12,8 @@ interface MangaRepository {
suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga>
suspend fun getFavorites(): List<Manga>
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga? suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga?

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.data.DatabaseHandler import eu.kanade.data.DatabaseHandler
import eu.kanade.data.toLong import eu.kanade.data.toLong
import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
@ -16,6 +17,7 @@ import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import data.Mangas as DbManga import data.Mangas as DbManga
import eu.kanade.domain.manga.model.Manga as DomainManga
abstract class AbstractBackupManager(protected val context: Context) { abstract class AbstractBackupManager(protected val context: Context) {
@ -24,6 +26,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
internal val sourceManager: SourceManager = Injekt.get() internal val sourceManager: SourceManager = Injekt.get()
internal val trackManager: TrackManager = Injekt.get() internal val trackManager: TrackManager = Injekt.get()
protected val preferences: PreferencesHelper = Injekt.get() protected val preferences: PreferencesHelper = Injekt.get()
private val getFavorites: GetFavorites = Injekt.get()
abstract suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String abstract suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
@ -60,8 +63,8 @@ abstract class AbstractBackupManager(protected val context: Context) {
* *
* @return [Manga] from library * @return [Manga] from library
*/ */
protected suspend fun getFavoriteManga(): List<DbManga> { protected suspend fun getFavoriteManga(): List<DomainManga> {
return handler.awaitList { mangasQueries.getFavorites() } return getFavorites.await()
} }
/** /**

View File

@ -38,6 +38,7 @@ import okio.sink
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Date import java.util.Date
import kotlin.math.max import kotlin.math.max
import eu.kanade.domain.manga.model.Manga as DomainManga
class FullBackupManager(context: Context) : AbstractBackupManager(context) { class FullBackupManager(context: Context) : AbstractBackupManager(context) {
@ -113,13 +114,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
} }
private suspend fun backupManga(mangas: List<Mangas>, flags: Int): List<BackupManga> { private suspend fun backupManga(mangas: List<DomainManga>, flags: Int): List<BackupManga> {
return mangas.map { return mangas.map {
backupMangaObject(it, flags) backupMangaObject(it, flags)
} }
} }
private fun backupExtensionInfo(mangas: List<Mangas>): List<BackupSource> { private fun backupExtensionInfo(mangas: List<DomainManga>): List<BackupSource> {
return mangas return mangas
.asSequence() .asSequence()
.map { it.source } .map { it.source }
@ -150,14 +151,14 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param options options for the backup * @param options options for the backup
* @return [BackupManga] containing manga in a serializable form * @return [BackupManga] containing manga in a serializable form
*/ */
private suspend fun backupMangaObject(manga: Mangas, options: Int): BackupManga { private suspend fun backupMangaObject(manga: DomainManga, options: Int): BackupManga {
// Entry for this manga // Entry for this manga
val mangaObject = BackupManga.copyFrom(manga) val mangaObject = BackupManga.copyFrom(manga)
// Check if user wants chapter information in backup // Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
// Backup all the chapters // Backup all the chapters
val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga._id, backupChapterMapper) } val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
mangaObject.chapters = chapters mangaObject.chapters = chapters
} }
@ -166,7 +167,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants category information in backup // Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga // Backup categories for this manga
val categoriesForManga = handler.awaitList { categoriesQueries.getCategoriesByMangaId(manga._id) } val categoriesForManga = handler.awaitList { categoriesQueries.getCategoriesByMangaId(manga.id) }
if (categoriesForManga.isNotEmpty()) { if (categoriesForManga.isNotEmpty()) {
mangaObject.categories = categoriesForManga.map { it.order } mangaObject.categories = categoriesForManga.map { it.order }
} }
@ -174,7 +175,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants track information in backup // Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga._id, backupTrackMapper) } val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
if (tracks.isNotEmpty()) { if (tracks.isNotEmpty()) {
mangaObject.tracking = tracks mangaObject.tracking = tracks
} }
@ -182,7 +183,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants history information in backup // Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
val historyByMangaId = handler.awaitList(true) { historyQueries.getHistoryByMangaId(manga._id) } val historyByMangaId = handler.awaitList(true) { historyQueries.getHistoryByMangaId(manga.id) }
if (historyByMangaId.isNotEmpty()) { if (historyByMangaId.isNotEmpty()) {
val history = historyByMangaId.map { history -> val history = historyByMangaId.map { history ->
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapter_id) } val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapter_id) }

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.backup.full.models package eu.kanade.tachiyomi.data.backup.full.models
import data.Mangas import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
@ -69,7 +69,7 @@ data class BackupManga(
} }
companion object { companion object {
fun copyFrom(manga: Mangas): BackupManga { fun copyFrom(manga: Manga): BackupManga {
return BackupManga( return BackupManga(
url = manga.url, url = manga.url,
title = manga.title, title = manga.title,
@ -78,13 +78,13 @@ data class BackupManga(
description = manga.description, description = manga.description,
genre = manga.genre ?: emptyList(), genre = manga.genre ?: emptyList(),
status = manga.status.toInt(), status = manga.status.toInt(),
thumbnailUrl = manga.thumbnail_url, thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite, favorite = manga.favorite,
source = manga.source, source = manga.source,
dateAdded = manga.date_added, dateAdded = manga.dateAdded,
viewer = (manga.viewer.toInt() and ReadingModeType.MASK), viewer = (manga.viewerFlags.toInt() and ReadingModeType.MASK),
viewer_flags = manga.viewer.toInt(), viewer_flags = manga.viewerFlags.toInt(),
chapterFlags = manga.chapter_flags.toInt(), chapterFlags = manga.chapterFlags.toInt(),
) )
} }
} }

View File

@ -8,17 +8,6 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable
interface ChapterQueries : DbProvider { interface ChapterQueries : DbProvider {
fun getChapters(mangaId: Long) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build(),
)
.prepare()
fun getChapter(id: Long) = db.get() fun getChapter(id: Long) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery( .withQuery(

View File

@ -173,7 +173,7 @@ class NotificationReceiver : BroadcastReceiver() {
val manga = db.getManga(mangaId).executeAsBlocking() val manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking() val chapter = db.getChapter(chapterId).executeAsBlocking()
if (manga != null && chapter != null) { if (manga != null && chapter != null) {
val intent = ReaderActivity.newIntent(context, manga, chapter).apply { val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
} }
context.startActivity(intent) context.startActivity(intent)
@ -440,7 +440,7 @@ class NotificationReceiver : BroadcastReceiver() {
* @param chapter chapter that needs to be opened * @param chapter chapter that needs to be opened
*/ */
internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter: Chapter): PendingIntent { internal fun openChapterPendingActivity(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
val newIntent = ReaderActivity.newIntent(context, manga, chapter) val newIntent = ReaderActivity.newIntent(context, manga.id, chapter.id)
return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) return PendingIntent.getActivity(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} }

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Bundle import android.os.Bundle
import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
@ -16,7 +16,7 @@ import uy.kohesive.injekt.api.get
class MigrationMangaPresenter( class MigrationMangaPresenter(
private val sourceId: Long, private val sourceId: Long,
private val getFavoritesBySourceId: GetFavoritesBySourceId = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(),
) : BasePresenter<MigrationMangaController>() { ) : BasePresenter<MigrationMangaController>() {
private val _state: MutableStateFlow<MigrateMangaState> = MutableStateFlow(MigrateMangaState.Loading) private val _state: MutableStateFlow<MigrateMangaState> = MutableStateFlow(MigrateMangaState.Loading)
@ -25,7 +25,7 @@ class MigrationMangaPresenter(
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
presenterScope.launchIO { presenterScope.launchIO {
getFavoritesBySourceId getFavorites
.subscribe(sourceId) .subscribe(sourceId)
.catch { exception -> .catch { exception ->
_state.value = MigrateMangaState.Error(exception) _state.value = MigrateMangaState.Error(exception)

View File

@ -46,7 +46,6 @@ import com.google.android.material.transition.platform.MaterialContainerTransfor
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
@ -106,10 +105,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
} }
} }
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return newIntent(context, manga.id, chapter.id)
}
private const val ENABLED_BUTTON_IMAGE_ALPHA = 255 private const val ENABLED_BUTTON_IMAGE_ALPHA = 255
private const val DISABLED_BUTTON_IMAGE_ALPHA = 64 private const val DISABLED_BUTTON_IMAGE_ALPHA = 64

View File

@ -5,11 +5,15 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.UpdateChapter import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.ChapterUpdate import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.history.interactor.UpsertHistory import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.model.HistoryUpdate import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
@ -41,6 +45,7 @@ import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.takeBytes import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
@ -68,6 +73,8 @@ class ReaderPresenter(
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(), private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(),
private val getMangaById: GetMangaById = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(), private val upsertHistory: UpsertHistory = Injekt.get(),
@ -118,14 +125,14 @@ class ReaderPresenter(
*/ */
private val chapterList by lazy { private val chapterList by lazy {
val manga = manga!! val manga = manga!!
val dbChapters = db.getChapters(manga.id!!).executeAsBlocking() val chapters = runBlocking { getChapterByMangaId.await(manga.id!!) }
val selectedChapter = dbChapters.find { it.id == chapterId } val selectedChapter = chapters.find { it.id == chapterId }
?: error("Requested chapter of id $chapterId not found in chapter list") ?: error("Requested chapter of id $chapterId not found in chapter list")
val chaptersForReader = when { val chaptersForReader = when {
(preferences.skipRead() || preferences.skipFiltered()) -> { (preferences.skipRead() || preferences.skipFiltered()) -> {
val filteredChapters = dbChapters.filterNot { val filteredChapters = chapters.filterNot {
when { when {
preferences.skipRead() && it.read -> true preferences.skipRead() && it.read -> true
preferences.skipFiltered() -> { preferences.skipFiltered() -> {
@ -146,10 +153,11 @@ class ReaderPresenter(
filteredChapters + listOf(selectedChapter) filteredChapters + listOf(selectedChapter)
} }
} }
else -> dbChapters else -> chapters
} }
chaptersForReader chaptersForReader
.map { it.toDbChapter() }
.sortedWith(getChapterSort(manga, sortDescending = false)) .sortedWith(getChapterSort(manga, sortDescending = false))
.map(::ReaderChapter) .map(::ReaderChapter)
} }
@ -232,16 +240,16 @@ class ReaderPresenter(
fun init(mangaId: Long, initialChapterId: Long) { fun init(mangaId: Long, initialChapterId: Long) {
if (!needsInit()) return if (!needsInit()) return
db.getManga(mangaId).asRxObservable() launchIO {
.first() try {
.observeOn(AndroidSchedulers.mainThread()) val manga = getMangaById.await(mangaId)
.doOnNext { init(it, initialChapterId) } withUIContext {
.subscribeFirst( manga?.let { init(it.toDbManga(), initialChapterId) }
{ _, _ -> }
// Ignore onNext event } catch (e: Throwable) {
}, view?.setInitialChapterError(e)
ReaderActivity::setInitialChapterError, }
) }
} }
/** /**