Restoring Kotatsu schema backups

This commit is contained in:
2024-03-28 00:19:59 +03:00
parent 91a3ddff4e
commit ff021c6eec
12 changed files with 204 additions and 299 deletions

View File

@@ -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>

View File

@@ -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
)
)
}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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*/ },

View File

@@ -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,114 +117,127 @@ fun HistoryView(
Box(
Modifier.fillMaxSize()
) {
Column(Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.collapsable(
state = scrollState,
topBarHeightPx = topBarHeightPx,
topBarOffsetY = topBarOffsetY
)
.padding(padding)
) {
animatedItemsIndexed(
state = animatedList.value,
key = { rowItem -> rowItem.key },
) { index, item ->
when (item.type) {
RowEntityType.Header -> ListGroupHeader(
calculateTimeAgo(item.day).format(
LocalContext.current.resources
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
.collapsable(
state = scrollState,
topBarHeightPx = topBarHeightPx,
topBarOffsetY = topBarOffsetY
)
)
RowEntityType.Item -> SwipeActions(
startActionsConfig = SwipeActionsConfig(
threshold = 0.33f,
background = MaterialTheme.colorScheme.errorContainer,
backgroundActive = MaterialTheme.colorScheme.error,
iconTint = MaterialTheme.colorScheme.onError,
icon = Icons.Outlined.DeleteForever,
stayDismissed = true,
onDismiss = {
viewModel.removeFromHistory(item.historyItemModel!!)
}
),
endActionsConfig = SwipeActionsConfig(
threshold = 0.33f,
background = MaterialTheme.colorScheme.tertiaryContainer,
backgroundActive = MaterialTheme.colorScheme.tertiary,
iconTint = MaterialTheme.colorScheme.onTertiary,
icon = Icons.Outlined.PlayArrow,
stayDismissed = false,
onDismiss = {
navigateToReader()
}
),
onTried = { isUserTrySwipe = true },
showTutorial = false,
) { state ->
val size = with(LocalDensity.current) {
java.lang.Float.max(
java.lang.Float.min(
16.dp.toPx(),
abs(state.offset.value)
), 0f
).toDp()
}
val animateCorners by remember {
derivedStateOf {
state.offset.value.absoluteValue > 30
}
}
val startCorners by animateDpAsState(
targetValue = when {
state.dismissDirection == DismissDirection.StartToEnd &&
animateCorners -> 8.dp
else -> 0.dp
}, label = "startCorners"
)
val endCorners by animateDpAsState(
targetValue = when {
state.dismissDirection == DismissDirection.EndToStart &&
animateCorners -> 8.dp
else -> 0.dp
}, label = "endCorners"
)
Box(
modifier = Modifier.height(IntrinsicSize.Min)
) {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(
vertical = min(
size / 4f,
4.dp
)
)
.clip(RoundedCornerShape(size)),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(
topStart = startCorners,
bottomStart = startCorners,
topEnd = endCorners,
bottomEnd = endCorners,
),
) {
// nothing
}
Box(
modifier = Modifier.padding(vertical = 4.dp)
) {
HistoryItem(
coil = coil,
history = item.historyItemModel!!,
onClick = { navigateToDetails(item.historyItemModel!!.manga.id) },
.padding(padding)
) {
animatedItemsIndexed(
state = animatedList.value,
key = { rowItem -> rowItem.key },
) { index, item ->
when (item.type) {
RowEntityType.Header -> ListGroupHeader(
calculateTimeAgo(item.day).format(
LocalContext.current.resources
)
)
RowEntityType.Item -> SwipeActions(
startActionsConfig = SwipeActionsConfig(
threshold = 0.33f,
background = MaterialTheme.colorScheme.errorContainer,
backgroundActive = MaterialTheme.colorScheme.error,
iconTint = MaterialTheme.colorScheme.onError,
icon = Icons.Outlined.DeleteForever,
stayDismissed = true,
onDismiss = {
viewModel.removeFromHistory(item.historyItemModel!!)
}
),
endActionsConfig = SwipeActionsConfig(
threshold = 0.33f,
background = MaterialTheme.colorScheme.tertiaryContainer,
backgroundActive = MaterialTheme.colorScheme.tertiary,
iconTint = MaterialTheme.colorScheme.onTertiary,
icon = Icons.Outlined.PlayArrow,
stayDismissed = false,
onDismiss = {
navigateToReader()
}
),
onTried = { isUserTrySwipe = true },
showTutorial = false,
) { state ->
val size = with(LocalDensity.current) {
java.lang.Float.max(
java.lang.Float.min(
16.dp.toPx(),
abs(state.offset.value)
), 0f
).toDp()
}
val animateCorners by remember {
derivedStateOf {
state.offset.value.absoluteValue > 30
}
}
val startCorners by animateDpAsState(
targetValue = when {
state.dismissDirection == DismissDirection.StartToEnd &&
animateCorners -> 8.dp
else -> 0.dp
}, label = "startCorners"
)
val endCorners by animateDpAsState(
targetValue = when {
state.dismissDirection == DismissDirection.EndToStart &&
animateCorners -> 8.dp
else -> 0.dp
}, label = "endCorners"
)
Box(
modifier = Modifier.height(IntrinsicSize.Min)
) {
Surface(
modifier = Modifier
.fillMaxSize()
.padding(
vertical = min(
size / 4f,
4.dp
)
)
.clip(RoundedCornerShape(size)),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(
topStart = startCorners,
bottomStart = startCorners,
topEnd = endCorners,
bottomEnd = endCorners,
),
) {
// nothing
}
Box(
modifier = Modifier.padding(vertical = 4.dp)
) {
HistoryItem(
coil = coil,
history = item.historyItemModel!!,
onClick = { navigateToDetails(item.historyItemModel!!.manga.id) },
)
}
}
}
}
}
@@ -231,12 +245,5 @@ fun HistoryView(
}
}
}
if (history.isEmpty()) {
EmptyScreen(
icon = Icons.Outlined.History,
title = R.string.empty_history_title,
description = R.string.empty_history_description
)
}
}
}

View File

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

View File

@@ -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,16 +64,16 @@ 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
) {
BackupItem(
title = it.toString()
)
for (item in items) {
item {
BackupItem(
title = item.name.name
)
}
}
item {
var isLoading by remember { mutableStateOf(false) }
@@ -89,7 +92,7 @@ fun RestoreItemsView(
icon = Icons.Outlined.Restore,
isLoading = isLoading
) {
restoreViewModel.restore()
// restoreViewModel.restore()
}
}
}

View File

@@ -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)) {
if (it.isChecked) it.name else null
}
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) {
backup.getEntry(BackupEntry.Name.HISTORY)?.let {
result += repository.restoreHistory(it)
}
//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) {
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let {
result += repository.restoreCategories(it)
}
//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) {
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let {
result += repository.restoreFavourites(it)
}
//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) {
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
result += repository.restoreBookmarks(it)
}
//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) {
backup.getEntry(BackupEntry.Name.SOURCES)?.let {
result += repository.restoreSources(it)
}
//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)
}
}
}

View File

@@ -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),
)
}

View File

@@ -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
)

View File

@@ -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