Restoring Kotatsu schema backups

master
Zakhar Timoshenko 2 years ago
parent 91a3ddff4e
commit ff021c6eec
Signed by: Xtimms
SSH Key Fingerprint: SHA256:wH6spYepK/A5erBh7ZyAnr1ru9H4eaMVBEuiw6DSpxI

@ -13,12 +13,12 @@
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\xtimms\.android\avd\Small_Phone_API_34.avd" />
<value value="C:\Users\xtimms\.android\avd\Pixel_API_27.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-03-17T20:36:24.681011800Z" />
<timeTargetWasSelectedWithDropDown value="2024-03-27T16:43:21.742325100Z" />
</State>
</entry>
</value>

@ -21,124 +21,5 @@ class DatabasePrePopulateCallback(private val resources: Resources) : RoomDataba
0L,
)
)
db.execSQL(
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track, show_in_lib, `deleted_at`) VALUES (?,?,?,?,?,?,?)",
arrayOf(
System.currentTimeMillis(),
1,
resources.getString(R.string.reading),
SortOrder.NEWEST.name,
1,
1,
0L,
)
)
db.execSQL(
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track, show_in_lib, `deleted_at`) VALUES (?,?,?,?,?,?,?)",
arrayOf(
System.currentTimeMillis(),
1,
resources.getString(R.string.completed),
SortOrder.NEWEST.name,
1,
1,
0L,
)
)
db.execSQL(
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track, show_in_lib, `deleted_at`) VALUES (?,?,?,?,?,?,?)",
arrayOf(
System.currentTimeMillis(),
1,
resources.getString(R.string.dropped),
SortOrder.NEWEST.name,
1,
1,
0L,
)
)
db.execSQL(
"INSERT INTO sources (source, enabled, sort_key) VALUES (?,?,?)",
arrayOf(
"MANGADEX",
1,
1,
)
)
db.execSQL(
"INSERT INTO sources (source, enabled, sort_key) VALUES (?,?,?)",
arrayOf(
"DESUME",
1,
1,
)
)
db.execSQL(
"INSERT INTO manga (manga_id, title, alt_title, url, public_url, rating, nsfw, cover_url, large_cover_url, state, author, source) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
arrayOf(
4427365311541330000,
"Seitokai ni mo Ana wa Aru!",
"",
"822c9883-385c-4fd0-9523-16e7789cbeae",
"https://mangadex.org/title/822c9883-385c-4fd0-9523-16e7789cbeae",
-1.0,
0,
"https://mangadex.org/covers/822c9883-385c-4fd0-9523-16e7789cbeae/f886822a-80c3-484c-ad75-9aa32abedc18.jpg.256.jpg",
"https://mangadex.org/covers/822c9883-385c-4fd0-9523-16e7789cbeae/f886822a-80c3-484c-ad75-9aa32abedc18.jpg",
"FINISHED",
"Muchi Maro",
"MANGADEX",
)
)
db.execSQL(
"INSERT INTO manga (manga_id, title, alt_title, url, public_url, rating, nsfw, cover_url, large_cover_url, state, author, source) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
arrayOf(
-5513532524243987690,
"Тотальный гарем",
"Shuumatsu no Harem",
"/manga/api/694",
"https://desu.me/manga/z-shuumatsu-no-harem.694/",
1.0,
1,
"https://desu.me/data/manga/covers/preview/694.jpg",
"https://desu.me/data/manga/covers/original/694.jpg",
"ONGOING",
"",
"DESUME",
)
)
db.execSQL(
"INSERT INTO favourites (manga_id, category_id, sort_key, created_at, deleted_at) VALUES (?,?,?,?,?)",
arrayOf(
4427365311541330000,
1,
0,
1705944302882,
0,
)
)
db.execSQL(
"INSERT INTO favourites (manga_id, category_id, sort_key, created_at, deleted_at) VALUES (?,?,?,?,?)",
arrayOf(
-5513532524243987690,
1,
0,
1705944302882,
0,
)
)
db.execSQL(
"INSERT into history (manga_id, created_at, updated_at, chapter_id, page, scroll, percent, deleted_at) VALUES (?,?,?,?,?,?,?,?)",
arrayOf(
-5513532524243987690,
1710617414,
1710617414,
1,
3,
0.3,
0.4,
0
)
)
}
}

@ -0,0 +1,18 @@
package org.xtimms.tokusho.core.screens
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}

@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.MenuBook
import androidx.compose.material.icons.outlined.Block

@ -251,7 +251,7 @@ fun DetailsView(
title = it.chapter.name,
date = it.chapter.uploadDate,
scanlator = it.chapter.scanlator,
read = it.isUnread,
read = !it.isUnread,
bookmark = false,
selected = false,
onLongClick = { /*TODO*/ },

@ -48,6 +48,7 @@ import org.xtimms.tokusho.core.components.effects.updateAnimatedItemsState
import org.xtimms.tokusho.core.prefs.AppSettings
import org.xtimms.tokusho.core.prefs.SWIPE_TUTORIAL
import org.xtimms.tokusho.core.screens.EmptyScreen
import org.xtimms.tokusho.core.screens.LoadingScreen
import org.xtimms.tokusho.utils.lang.calculateTimeAgo
import org.xtimms.tokusho.utils.lang.isSameDay
import java.time.Instant
@ -70,11 +71,11 @@ fun HistoryView(
val scrollState = rememberScrollState()
var isUserTrySwipe by remember { mutableStateOf(false) }
val history by viewModel.content.collectAsStateWithLifecycle(emptyList())
val history by viewModel.content.collectAsStateWithLifecycle(null)
DisposableEffect(Unit) {
onDispose {
if (history.isNotEmpty() && isUserTrySwipe) {
if (history?.isNotEmpty() == true && isUserTrySwipe) {
AppSettings.updateValue(SWIPE_TUTORIAL, isUserTrySwipe)
}
}
@ -83,7 +84,7 @@ fun HistoryView(
val animatedList = run {
val list = emptyList<RowEntity>().toMutableList()
var readDate: Instant? = null
history.forEach { item ->
history?.forEach { item ->
if (readDate === null || !isSameDay(
item.history.updatedAt.toEpochMilli(),
@ -116,6 +117,16 @@ fun HistoryView(
Box(
Modifier.fillMaxSize()
) {
history.let {
if (it == null) {
LoadingScreen(Modifier.padding(padding))
} else if (it.isEmpty()) {
EmptyScreen(
icon = Icons.Outlined.History,
title = R.string.empty_history_title,
description = R.string.empty_history_description
)
} else {
Column(Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
@ -136,6 +147,7 @@ fun HistoryView(
LocalContext.current.resources
)
)
RowEntityType.Item -> SwipeActions(
startActionsConfig = SwipeActionsConfig(
threshold = 0.33f,
@ -231,12 +243,7 @@ fun HistoryView(
}
}
}
if (history.isEmpty()) {
EmptyScreen(
icon = Icons.Outlined.History,
title = R.string.empty_history_title,
description = R.string.empty_history_description
)
}
}
}
}

@ -108,7 +108,7 @@ fun BackupRestoreView(
return@rememberLauncherForActivityResult
}
navigateToRestoreScreen(uri.toString())
restoreViewModel.restore(uri)
}
val showDirectoryAlert =

@ -9,10 +9,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccessTime
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -28,7 +30,7 @@ import org.xtimms.tokusho.core.components.ScaffoldWithTopAppBar
import org.xtimms.tokusho.sections.settings.about.ProgressIndicatorButton
import org.xtimms.tokusho.utils.DeviceUtil
const val RESTORE_ARGUMENT = "{source}"
const val RESTORE_ARGUMENT = "{file}"
const val RESTORE_DESTINATION = "restore/?file=${RESTORE_ARGUMENT}"
@Composable
@ -38,7 +40,8 @@ fun RestoreItemsView(
navigateBack: () -> Unit,
) {
val items = restoreViewModel.availableEntries.collectAsStateWithLifecycle()
val items by restoreViewModel.availableEntries.collectAsStateWithLifecycle(emptyList())
val backupDate by restoreViewModel.backupDate.collectAsStateWithLifecycle(null)
ScaffoldWithTopAppBar(
title = stringResource(R.string.restore_from_backup),
@ -61,17 +64,17 @@ fun RestoreItemsView(
item {
PreferencesHintCard(
title = stringResource(id = R.string.backup_creation_date),
description = restoreViewModel.backupDate.value.toString(),
description = backupDate.toString(),
icon = Icons.Outlined.AccessTime
)
}
items(
count = 5
) {
for (item in items) {
item {
BackupItem(
title = it.toString()
title = item.name.name
)
}
}
item {
var isLoading by remember { mutableStateOf(false) }
Row(
@ -89,7 +92,7 @@ fun RestoreItemsView(
icon = Icons.Outlined.Restore,
isLoading = isLoading
) {
restoreViewModel.restore()
// restoreViewModel.restore()
}
}
}

@ -1,6 +1,7 @@
package org.xtimms.tokusho.sections.settings.backup
import android.content.Context
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
@ -25,13 +26,14 @@ import javax.inject.Inject
@HiltViewModel
class RestoreViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val savedStateHandle: SavedStateHandle,
private val repository: BackupRepository,
@ApplicationContext context: Context,
@ApplicationContext val context: Context,
) : KotatsuBaseViewModel() {
private val backupInput = SuspendLazy {
val uri = savedStateHandle.get<String>(RESTORE_ARGUMENT)?.toUriOrNull() ?: throw FileNotFoundException()
val uri = savedStateHandle.get<String>(RESTORE_ARGUMENT)?.toUriOrNull()
?: throw FileNotFoundException()
val contentResolver = context.contentResolver
runInterruptible(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp")
@ -74,57 +76,68 @@ class RestoreViewModel @Inject constructor(
}
fun onItemClick(item: BackupEntryModel) {
val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
val map =
availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
map[item.name] = item.copy(isChecked = !item.isChecked)
map.validate()
availableEntries.value = map.values.sortedBy { it.name.ordinal }
}
fun restore() {
fun restore(uri: Uri) {
launchLoadingJob {
val backup = backupInput.get()
val checkedItems = availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
val contentResolver = context.contentResolver
val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
val backupInput = BackupZipInput(tempFile)
val backup: BackupZipInput = backupInput
val checkedItems =
availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
if (it.isChecked) it.name else null
}
val result = CompositeResult()
val step = 1f / 5f
progress.value = 0f
if (BackupEntry.Name.HISTORY in checkedItems) {
//if (BackupEntry.Name.HISTORY in checkedItems) {
backup.getEntry(BackupEntry.Name.HISTORY)?.let {
result += repository.restoreHistory(it)
}
}
//}
progress.value += step
if (BackupEntry.Name.CATEGORIES in checkedItems) {
//if (BackupEntry.Name.CATEGORIES in checkedItems) {
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let {
result += repository.restoreCategories(it)
}
}
//}
progress.value += step
if (BackupEntry.Name.FAVOURITES in checkedItems) {
//if (BackupEntry.Name.FAVOURITES in checkedItems) {
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let {
result += repository.restoreFavourites(it)
}
}
//}
progress.value += step
if (BackupEntry.Name.BOOKMARKS in checkedItems) {
//if (BackupEntry.Name.BOOKMARKS in checkedItems) {
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
result += repository.restoreBookmarks(it)
}
}
//}
progress.value += step
if (BackupEntry.Name.SOURCES in checkedItems) {
//if (BackupEntry.Name.SOURCES in checkedItems) {
backup.getEntry(BackupEntry.Name.SOURCES)?.let {
result += repository.restoreSources(it)
}
}
//}
progress.value = 1f
backup.cleanupAsync()
onRestoreDone.call(result)
}
}
@ -142,7 +155,8 @@ class RestoreViewModel @Inject constructor(
}
} else {
if (favorites.isEnabled) {
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = false, isChecked = false)
this[BackupEntry.Name.FAVOURITES] =
favorites.copy(isEnabled = false, isChecked = false)
}
}
}

@ -25,7 +25,6 @@ fun ShelfPager(
coil: ImageLoader,
state: PagerState,
contentPadding: PaddingValues,
searchQuery: String?,
getShelfForPage: (Int) -> List<ShelfManga>,
navigateToDetails: (ShelfManga) -> Unit,
) {
@ -41,7 +40,6 @@ fun ShelfPager(
val library = getShelfForPage(page)
if (library.isEmpty()) {
ShelfPagerEmptyScreen(
searchQuery = searchQuery,
contentPadding = contentPadding,
)
return@HorizontalPager
@ -61,14 +59,8 @@ fun ShelfPager(
@Composable
private fun ShelfPagerEmptyScreen(
searchQuery: String?,
contentPadding: PaddingValues,
) {
val msg = when {
!searchQuery.isNullOrEmpty() -> R.string.no_results_found
else -> R.string.information_no_manga_category
}
Column(
modifier = Modifier
.padding(contentPadding + PaddingValues(8.dp))
@ -78,7 +70,7 @@ private fun ShelfPagerEmptyScreen(
EmptyScreen(
icon = Icons.Outlined.Close,
title = R.string.empty_here,
description = msg,
description = R.string.information_no_manga_category,
modifier = Modifier.weight(1f),
)
}

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -110,7 +109,6 @@ fun ShelfViewContent(
coil = coil,
state = pagerState,
contentPadding = PaddingValues(bottom = padding.calculateBottomPadding()),
searchQuery = "",
getShelfForPage = { mangas },
navigateToDetails = onClickManga
)

@ -3,18 +3,12 @@ package org.xtimms.tokusho.sections.shelf
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.xtimms.tokusho.core.base.viewmodel.BaseViewModel
import org.xtimms.tokusho.core.base.viewmodel.KotatsuBaseViewModel
import org.xtimms.tokusho.data.repository.FavouritesRepository
import org.xtimms.tokusho.utils.lang.mapItems
@ -28,11 +22,10 @@ class ShelfViewModel @Inject constructor(
private val mangasStateFlow = favouritesRepository.observeAll(1)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
private val categoriesStateFlow = favouritesRepository.observeCategoriesForLibrary()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val mangaCount = favouritesRepository.observeMangaCount()
val mangaCount = favouritesRepository.observeMangaCountInCategory(1)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val categories = categoriesStateFlow

Loading…
Cancel
Save