Decentralize database

This commit is contained in:
tom5079
2021-12-27 11:57:26 +09:00
parent c3c5761ffa
commit 850ac3ea83
21 changed files with 226 additions and 202 deletions

View File

@@ -45,7 +45,6 @@ import okhttp3.Protocol
import org.kodein.di.* import org.kodein.di.*
import org.kodein.di.android.x.androidXModule import org.kodein.di.android.x.androidXModule
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.db.databaseModule
import xyz.quaver.pupil.sources.sourceModule import xyz.quaver.pupil.sources.sourceModule
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.util.* import java.util.*
@@ -54,7 +53,6 @@ class Pupil : Application(), DIAware {
override val di: DI by DI.lazy { override val di: DI by DI.lazy {
import(androidXModule(this@Pupil)) import(androidXModule(this@Pupil))
import(databaseModule)
import(sourceModule) import(sourceModule)
bind { singleton { NetworkCache(applicationContext) } } bind { singleton { NetworkCache(applicationContext) } }

View File

@@ -1,34 +0,0 @@
package xyz.quaver.pupil.db
import androidx.lifecycle.LiveData
import androidx.room.*
@Entity(primaryKeys = ["source", "itemID"])
data class Bookmark(
val source: String,
val itemID: String,
val timestamp: Long = System.currentTimeMillis()
)
@Dao
interface BookmarkDao {
@Query("SELECT * FROM bookmark")
fun getAll(): LiveData<List<Bookmark>>
@Query("SELECT itemID FROM bookmark WHERE source = :source")
fun getAll(source: String): LiveData<List<String>>
@Query("SELECT EXISTS(SELECT * FROM bookmark WHERE source = :source AND itemID = :itemID)")
fun contains(source: String, itemID: String): LiveData<Boolean>
fun contains(bookmark: Bookmark) = contains(bookmark.source, bookmark.itemID)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(bookmark: Bookmark)
suspend fun insert(source: String, itemID: String) = insert(Bookmark(source, itemID))
@Delete
suspend fun delete(bookmark: Bookmark)
suspend fun delete(source: String, itemID: String) = delete(Bookmark(source, itemID))
}

View File

@@ -1,17 +0,0 @@
package xyz.quaver.pupil.db
import android.app.Application
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import org.kodein.di.*
@Database(entities = [History::class, Bookmark::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
abstract fun bookmarkDao(): BookmarkDao
}
val databaseModule = DI.Module("database") {
bind<AppDatabase>() with singleton { Room.databaseBuilder(instance<Application>(), AppDatabase::class.java, "pupil").build() }
}

View File

@@ -1,26 +0,0 @@
package xyz.quaver.pupil.db
import androidx.lifecycle.LiveData
import androidx.room.*
@Entity(primaryKeys = ["source", "itemID"])
data class History(
val source: String,
val itemID: String,
val timestamp: Long = System.currentTimeMillis()
)
@Dao
interface HistoryDao {
@Query("SELECT * FROM history")
fun getAll(): LiveData<List<History>>
@Query("SELECT itemID FROM history WHERE source = :source")
fun getAll(source: String): LiveData<List<String>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(history: History)
@Delete
suspend fun delete(history: History)
}

View File

@@ -20,12 +20,8 @@ package xyz.quaver.pupil.sources.composable
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import android.os.Parcelable
import android.util.Log
import android.view.MotionEvent
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -46,13 +42,11 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@@ -79,25 +73,22 @@ import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import org.kodein.log.LoggerFactory import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger import org.kodein.log.newLogger
import xyz.quaver.graphics.subsampledimage.* import xyz.quaver.graphics.subsampledimage.ImageSource
import xyz.quaver.graphics.subsampledimage.ScaleTypes.CENTER_INSIDE import xyz.quaver.graphics.subsampledimage.SubSampledImage
import xyz.quaver.graphics.subsampledimage.SubSampledImageState
import xyz.quaver.graphics.subsampledimage.rememberSubSampledImageState
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.proto.ReaderOptions import xyz.quaver.pupil.proto.ReaderOptions
import xyz.quaver.pupil.proto.settingsDataStore import xyz.quaver.pupil.proto.settingsDataStore
import xyz.quaver.pupil.ui.theme.Orange500
import xyz.quaver.pupil.util.FileXImageSource import xyz.quaver.pupil.util.FileXImageSource
import xyz.quaver.pupil.util.NetworkCache import xyz.quaver.pupil.util.NetworkCache
import xyz.quaver.pupil.util.activity import xyz.quaver.pupil.util.activity
import xyz.quaver.pupil.util.rememberFileXImageSource
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.sign import kotlin.math.sign
@@ -189,8 +180,6 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
var fullscreen by mutableStateOf(false) var fullscreen by mutableStateOf(false)
private val database: AppDatabase by instance()
var error by mutableStateOf(false) var error by mutableStateOf(false)
var imageCount by mutableStateOf(0) var imageCount by mutableStateOf(0)

View File

@@ -51,6 +51,7 @@ import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.google.accompanist.insets.LocalWindowInsets import com.google.accompanist.insets.LocalWindowInsets
@@ -67,7 +68,7 @@ private enum class NavigationIconState {
ARROW ARROW
} }
open class SearchBaseViewModel<T>(app: Application) : AndroidViewModel(app) { open class SearchBaseViewModel<T> : ViewModel() {
val searchResults = mutableStateListOf<T>() val searchResults = mutableStateListOf<T>()
var sortModeIndex by mutableStateOf(0) var sortModeIndex by mutableStateOf(0)

View File

@@ -0,0 +1,50 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.sources.hitomi
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Entity
data class Favorite(
@PrimaryKey val item: String,
val timestamp: Long = System.currentTimeMillis()
)
@Dao
interface FavoritesDao {
@Query("SELECT * FROM favorite")
fun getAll(): Flow<List<Favorite>>
@Query("SELECT EXISTS(SELECT * FROM favorite WHERE item = :item)")
fun contains(item: String): Flow<Boolean>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(favorite: Favorite)
suspend fun insert(item: String) = insert(Favorite(item))
@Delete
suspend fun delete(favorite: Favorite)
suspend fun delete(item: String) = delete(Favorite(item))
}
@Database(entities = [Favorite::class], version = 1)
abstract class HitomiDatabase : RoomDatabase() {
abstract fun favoritesDao(): FavoritesDao
}

View File

@@ -33,7 +33,6 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -47,20 +46,22 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation import androidx.navigation.compose.navigation
import androidx.room.Room
import com.google.accompanist.insets.LocalWindowInsets import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.rememberInsetsPaddingValues import com.google.accompanist.insets.rememberInsetsPaddingValues
import com.google.accompanist.insets.ui.Scaffold import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.*
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.android.subDI
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import org.kodein.di.instance import org.kodein.di.compose.rememberViewModel
import org.kodein.di.compose.withDI
import org.kodein.log.LoggerFactory import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger import org.kodein.log.newLogger
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.proto.settingsDataStore import xyz.quaver.pupil.proto.settingsDataStore
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.composable.* import xyz.quaver.pupil.sources.composable.*
@@ -70,38 +71,42 @@ import xyz.quaver.pupil.sources.hitomi.lib.getGalleryInfo
import xyz.quaver.pupil.sources.hitomi.lib.getReferer import xyz.quaver.pupil.sources.hitomi.lib.getReferer
import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage
import xyz.quaver.pupil.ui.theme.Orange500 import xyz.quaver.pupil.ui.theme.Orange500
import java.util.*
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
class Hitomi(app: Application) : Source(), DIAware { class Hitomi(app: Application) : Source(), DIAware {
override val di by closestDI(app) override val di by subDI(closestDI(app)) {
bindSingleton {
Room.databaseBuilder(app, HitomiDatabase::class.java, name).build()
}
bindProvider { HitomiSearchResultViewModel(instance()) }
}
private val client: HttpClient by instance() private val client: HttpClient by instance()
private val logger = newLogger(LoggerFactory.default) private val logger = newLogger(LoggerFactory.default)
private val database: AppDatabase by instance()
private val bookmarkDao = database.bookmarkDao()
override val name: String = "hitomi.la" override val name: String = "hitomi.la"
override val iconResID: Int = R.drawable.hitomi override val iconResID: Int = R.drawable.hitomi
override fun NavGraphBuilder.navGraph(navController: NavController) { override fun NavGraphBuilder.navGraph(navController: NavController) {
navigation(startDestination = "hitomi.la/search", route = name) { navigation(startDestination = "hitomi.la/search", route = name) {
composable("hitomi.la/search") { Search(navController) } composable("hitomi.la/search") { withDI(di) { Search(navController) } }
composable("hitomi.la/reader/{itemID}") { Reader(navController) } composable("hitomi.la/reader/{itemID}") { withDI(di) { Reader(navController) } }
} }
} }
@Composable @Composable
fun Search(navController: NavController) { fun Search(navController: NavController) {
val model: HitomiSearchResultViewModel = viewModel() val model: HitomiSearchResultViewModel by rememberViewModel()
val database: AppDatabase by rememberInstance() val database: HitomiDatabase by rememberInstance()
val bookmarkDao = remember { database.bookmarkDao() } val favoritesDao = remember { database.favoritesDao() }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val bookmarks by bookmarkDao.getAll(name).observeAsState() val favorites by favoritesDao.getAll().collectAsState(emptyList())
val bookmarkSet by derivedStateOf { val favoritesSet by derivedStateOf {
bookmarks?.toSet() ?: emptySet() Collections.unmodifiableSet(favorites.mapTo(mutableSetOf()) { it.item })
} }
val context = LocalContext.current val context = LocalContext.current
@@ -200,11 +205,11 @@ class Hitomi(app: Application) : Source(), DIAware {
items(model.searchResults) { items(model.searchResults) {
DetailedSearchResult( DetailedSearchResult(
it, it,
bookmarks = bookmarkSet, favorites = favoritesSet,
onBookmarkToggle = { onFavoriteToggle = {
coroutineScope.launch { coroutineScope.launch {
if (it in bookmarkSet) bookmarkDao.delete(name, it) if (it in favoritesSet) favoritesDao.delete(it)
else bookmarkDao.insert(name, it) else favoritesDao.insert(it)
} }
} }
) { result -> ) { result ->
@@ -219,8 +224,8 @@ class Hitomi(app: Application) : Source(), DIAware {
fun Reader(navController: NavController) { fun Reader(navController: NavController) {
val model: ReaderBaseViewModel = viewModel() val model: ReaderBaseViewModel = viewModel()
val database: AppDatabase by rememberInstance() val database: HitomiDatabase by rememberInstance()
val bookmarkDao = database.bookmarkDao() val favoritesDao = remember { database.favoritesDao() }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -228,7 +233,7 @@ class Hitomi(app: Application) : Source(), DIAware {
if (itemID == null) model.error = true if (itemID == null) model.error = true
val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false) val isFavorite by favoritesDao.contains(itemID ?: "").collectAsState(false)
val galleryInfo by produceState<GalleryInfo?>(null) { val galleryInfo by produceState<GalleryInfo?>(null) {
runCatching { runCatching {
val galleryID = itemID!!.toInt() val galleryID = itemID!!.toInt()
@@ -271,13 +276,13 @@ class Hitomi(app: Application) : Source(), DIAware {
IconButton(onClick = { IconButton(onClick = {
itemID?.let { itemID?.let {
coroutineScope.launch { coroutineScope.launch {
if (bookmark) bookmarkDao.delete(name, it) if (isFavorite) favoritesDao.delete(it)
else bookmarkDao.insert(name, it) else favoritesDao.insert(it)
} }
} }
}) { }) {
Icon( Icon(
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline, if (isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
contentDescription = null, contentDescription = null,
tint = Orange500 tint = Orange500
) )

View File

@@ -18,7 +18,6 @@
package xyz.quaver.pupil.sources.hitomi package xyz.quaver.pupil.sources.hitomi
import android.app.Application
import android.util.LruCache import android.util.LruCache
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -29,12 +28,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.sources.composable.SearchBaseViewModel import xyz.quaver.pupil.sources.composable.SearchBaseViewModel
import xyz.quaver.pupil.sources.hitomi.lib.GalleryBlock import xyz.quaver.pupil.sources.hitomi.lib.GalleryBlock
import xyz.quaver.pupil.sources.hitomi.lib.doSearch import xyz.quaver.pupil.sources.hitomi.lib.doSearch
@@ -43,16 +36,9 @@ import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class HitomiSearchResultViewModel(app: Application) : SearchBaseViewModel<HitomiSearchResult>(app), DIAware { class HitomiSearchResultViewModel(
override val di by closestDI(app) private val client: HttpClient
) : SearchBaseViewModel<HitomiSearchResult>() {
private val logger = newLogger(LoggerFactory.default)
private val client: HttpClient by instance()
private val database: AppDatabase by instance()
private val bookmarkDao = database.bookmarkDao()
private var cachedQuery: String? = null private var cachedQuery: String? = null
private var cachedSortByPopularity: Boolean? = null private var cachedSortByPopularity: Boolean? = null
private val cache = mutableListOf<Int>() private val cache = mutableListOf<Int>()

View File

@@ -19,7 +19,6 @@
package xyz.quaver.pupil.sources.hitomi.composable package xyz.quaver.pupil.sources.hitomi.composable
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -101,8 +100,8 @@ private fun String.wordCapitalize() : String {
@Composable @Composable
fun DetailedSearchResult( fun DetailedSearchResult(
result: HitomiSearchResult, result: HitomiSearchResult,
bookmarks: Set<String>, favorites: Set<String>,
onBookmarkToggle: (String) -> Unit = { }, onFavoriteToggle: (String) -> Unit = { },
onClick: (HitomiSearchResult) -> Unit = { } onClick: (HitomiSearchResult) -> Unit = { }
) { ) {
val painter = rememberImagePainter(result.thumbnail) val painter = rememberImagePainter(result.thumbnail)
@@ -169,8 +168,8 @@ fun DetailedSearchResult(
key(result.tags) { key(result.tags) {
TagGroup( TagGroup(
tags = result.tags, tags = result.tags,
bookmarks, favorites,
onBookmarkToggle = onBookmarkToggle onFavoriteToggle = onFavoriteToggle
) )
} }
} }
@@ -192,13 +191,13 @@ fun DetailedSearchResult(
) )
Icon( Icon(
if (result.itemID in bookmarks) Icons.Default.Star else Icons.Default.StarOutline, if (result.itemID in favorites) Icons.Default.Star else Icons.Default.StarOutline,
contentDescription = null, contentDescription = null,
tint = Orange500, tint = Orange500,
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.clickable { .clickable {
onBookmarkToggle(result.itemID) onFavoriteToggle(result.itemID)
} }
) )
} }
@@ -210,20 +209,20 @@ fun DetailedSearchResult(
@Composable @Composable
fun TagGroup( fun TagGroup(
tags: List<String>, tags: List<String>,
bookmarks: Set<String>, favorites: Set<String>,
onBookmarkToggle: (String) -> Unit = { } onFavoriteToggle: (String) -> Unit = { }
) { ) {
var isFolded by remember { mutableStateOf(true) } var isFolded by remember { mutableStateOf(true) }
val bookmarkedTagsInList = bookmarks intersect tags.toSet() val favoriteTagsInList = favorites intersect tags.toSet()
FlowRow(Modifier.padding(0.dp, 16.dp)) { FlowRow(Modifier.padding(0.dp, 16.dp)) {
tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 } tags.sortedBy { if (favoriteTagsInList.contains(it)) 0 else 1 }
.let { (if (isFolded) it.take(10) else it) }.forEach { tag -> .let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
TagChip( TagChip(
tag = tag, tag = tag,
isFavorite = bookmarkedTagsInList.contains(tag), isFavorite = favoriteTagsInList.contains(tag),
onFavoriteClick = onBookmarkToggle onFavoriteClick = onFavoriteToggle
) )
} }

View File

@@ -0,0 +1,70 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.sources.manatoki
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Entity
data class Favorite(
@PrimaryKey val itemID: String
)
@Entity
data class Bookmark(
@PrimaryKey val itemID: String,
val page: Int
)
@Entity
data class History(
@PrimaryKey val itemID: String,
val page: Int
)
@Dao
interface FavoriteDao {
@Query("SELECT EXISTS(SELECT * FROM favorite WHERE itemID = :itemID)")
fun contains(itemID: String): Flow<Boolean>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(favorite: Favorite)
suspend fun insert(itemID: String) = insert(Favorite(itemID))
@Delete
suspend fun delete(favorite: Favorite)
suspend fun delete(itemID: String) = delete(Favorite(itemID))
}
@Dao
interface BookmarkDao {
}
@Dao
interface HistoryDao {
}
@Database(entities = [Favorite::class, Bookmark::class, History::class], version = 1)
abstract class ManatokiDatabase: RoomDatabase() {
abstract fun favoriteDao(): FavoriteDao
abstract fun bookmarkDao(): BookmarkDao
abstract fun historyDao(): HistoryDao
}

View File

@@ -32,16 +32,27 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navigation import androidx.navigation.navigation
import androidx.room.Room
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.android.subDI
import org.kodein.di.bindProvider
import org.kodein.di.bindSingleton
import org.kodein.di.compose.withDI
import org.kodein.di.instance
import org.kodein.log.LoggerFactory import org.kodein.log.LoggerFactory
import org.kodein.log.frontend.defaultLogFrontend
import org.kodein.log.newLogger import org.kodein.log.newLogger
import org.kodein.log.withShortPackageKeepLast
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.manatoki.composable.Main import xyz.quaver.pupil.sources.manatoki.composable.Main
import xyz.quaver.pupil.sources.manatoki.composable.Reader import xyz.quaver.pupil.sources.manatoki.composable.Reader
import xyz.quaver.pupil.sources.manatoki.composable.Recent import xyz.quaver.pupil.sources.manatoki.composable.Recent
import xyz.quaver.pupil.sources.manatoki.composable.Search import xyz.quaver.pupil.sources.manatoki.composable.Search
import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel
import xyz.quaver.pupil.sources.manatoki.viewmodel.SearchViewModel
@OptIn( @OptIn(
ExperimentalMaterialApi::class, ExperimentalMaterialApi::class,
@@ -50,19 +61,27 @@ import xyz.quaver.pupil.sources.manatoki.composable.Search
ExperimentalComposeUiApi::class ExperimentalComposeUiApi::class
) )
class Manatoki(app: Application) : Source(), DIAware { class Manatoki(app: Application) : Source(), DIAware {
override val di by closestDI(app) override val di by subDI(closestDI(app)) {
bindSingleton {
Room.databaseBuilder(
app, ManatokiDatabase::class.java, name
).build()
}
private val logger = newLogger(LoggerFactory.default) bindProvider { MainViewModel(instance()) }
bindProvider { RecentViewModel(instance()) }
bindProvider { SearchViewModel(instance()) }
}
override val name = "manatoki.net" override val name = "manatoki.net"
override val iconResID = R.drawable.manatoki override val iconResID = R.drawable.manatoki
override fun NavGraphBuilder.navGraph(navController: NavController) { override fun NavGraphBuilder.navGraph(navController: NavController) {
navigation(route = name, startDestination = "manatoki.net/") { navigation(route = name, startDestination = "manatoki.net/") {
composable("manatoki.net/") { Main(navController) } composable("manatoki.net/") { withDI(di) { Main(navController) } }
composable("manatoki.net/reader/{itemID}") { Reader(navController) } composable("manatoki.net/reader/{itemID}") { withDI(di) { Reader(navController) } }
composable("manatoki.net/search") { Search(navController) } composable("manatoki.net/search") { withDI(di) { Search(navController) } }
composable("manatoki.net/recent") { Recent(navController) } composable("manatoki.net/recent") { withDI(di) { Recent(navController) } }
} }
} }

View File

@@ -18,7 +18,6 @@
package xyz.quaver.pupil.sources.manatoki.composable package xyz.quaver.pupil.sources.manatoki.composable
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -40,7 +39,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.accompanist.insets.LocalWindowInsets import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.navigationBarsPadding
@@ -48,9 +46,9 @@ import com.google.accompanist.insets.rememberInsetsPaddingValues
import com.google.accompanist.insets.ui.Scaffold import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.rememberViewModel
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.proto.settingsDataStore import xyz.quaver.pupil.proto.settingsDataStore
import xyz.quaver.pupil.sources.composable.SourceSelectDialog import xyz.quaver.pupil.sources.composable.SourceSelectDialog
@@ -62,7 +60,7 @@ import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
@ExperimentalMaterialApi @ExperimentalMaterialApi
@Composable @Composable
fun Main(navController: NavController) { fun Main(navController: NavController) {
val model: MainViewModel = viewModel() val model: MainViewModel by rememberViewModel()
val client: HttpClient by rememberInstance() val client: HttpClient by rememberInstance()

View File

@@ -34,7 +34,6 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -57,10 +56,11 @@ import io.ktor.client.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.rememberViewModel
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.sources.composable.ReaderBase import xyz.quaver.pupil.sources.composable.ReaderBase
import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
import xyz.quaver.pupil.sources.manatoki.ManatokiDatabase
import xyz.quaver.pupil.sources.manatoki.MangaListing import xyz.quaver.pupil.sources.manatoki.MangaListing
import xyz.quaver.pupil.sources.manatoki.ReaderInfo import xyz.quaver.pupil.sources.manatoki.ReaderInfo
import xyz.quaver.pupil.sources.manatoki.getItem import xyz.quaver.pupil.sources.manatoki.getItem
@@ -79,8 +79,9 @@ fun Reader(navController: NavController) {
val client: HttpClient by rememberInstance() val client: HttpClient by rememberInstance()
val database: AppDatabase by rememberInstance() val database: ManatokiDatabase by rememberInstance()
val bookmarkDao = database.bookmarkDao() val favoriteDao = remember { database.favoriteDao() }
val bookmarkDao = remember { database.bookmarkDao() }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -98,7 +99,7 @@ fun Reader(navController: NavController) {
else model.error = true else model.error = true
} }
val bookmark by bookmarkDao.contains("manatoki.net", itemID ?: "").observeAsState(false) val isFavorite by favoriteDao.contains(itemID ?: "").collectAsState(false)
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) } var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
@@ -174,13 +175,13 @@ fun Reader(navController: NavController) {
IconButton(onClick = { IconButton(onClick = {
itemID?.let { itemID?.let {
coroutineScope.launch { coroutineScope.launch {
if (bookmark) bookmarkDao.delete("manatoki.net", it) if (isFavorite) favoriteDao.delete(it)
else bookmarkDao.insert("manatoki.net", it) else favoriteDao.insert(it)
} }
} }
}) { }) {
Icon( Icon(
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline, if (isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
contentDescription = null, contentDescription = null,
tint = Orange500 tint = Orange500
) )

View File

@@ -42,6 +42,7 @@ import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.rememberViewModel
import xyz.quaver.pupil.sources.composable.OverscrollPager import xyz.quaver.pupil.sources.composable.OverscrollPager
import xyz.quaver.pupil.sources.manatoki.MangaListing import xyz.quaver.pupil.sources.manatoki.MangaListing
import xyz.quaver.pupil.sources.manatoki.getItem import xyz.quaver.pupil.sources.manatoki.getItem
@@ -51,7 +52,7 @@ import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel
@ExperimentalMaterialApi @ExperimentalMaterialApi
@Composable @Composable
fun Recent(navController: NavController) { fun Recent(navController: NavController) {
val model: RecentViewModel = viewModel() val model: RecentViewModel by rememberViewModel()
val client: HttpClient by rememberInstance() val client: HttpClient by rememberInstance()

View File

@@ -58,6 +58,7 @@ import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.* import io.ktor.client.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.rememberViewModel
import xyz.quaver.pupil.sources.composable.ModalTopSheetLayout import xyz.quaver.pupil.sources.composable.ModalTopSheetLayout
import xyz.quaver.pupil.sources.composable.ModalTopSheetState import xyz.quaver.pupil.sources.composable.ModalTopSheetState
import xyz.quaver.pupil.sources.composable.OverscrollPager import xyz.quaver.pupil.sources.composable.OverscrollPager
@@ -70,7 +71,7 @@ import xyz.quaver.pupil.sources.manatoki.viewmodel.*
@ExperimentalMaterialApi @ExperimentalMaterialApi
@Composable @Composable
fun Search(navController: NavController) { fun Search(navController: NavController) {
val model: SearchViewModel = viewModel() val model: SearchViewModel by rememberViewModel()
val client: HttpClient by rememberInstance() val client: HttpClient by rememberInstance()

View File

@@ -144,7 +144,7 @@ suspend fun HttpClient.getItem(
}.toString() }.toString()
val urls = Jsoup.parse(htmlData) val urls = Jsoup.parse(htmlData)
.select("img[^data-]:not([style]):not([src*=loading])").also { Log.d("PUPILD", it.size.toString()) } .select("img[^data-]:not([style])")
.map { .map {
it.attributes() it.attributes()
.first { it.key.startsWith("data-") } .first { it.key.startsWith("data-") }

View File

@@ -21,6 +21,7 @@ package xyz.quaver.pupil.sources.manatoki.viewmodel
import android.app.Application import android.app.Application
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
@@ -46,13 +47,9 @@ data class TopWeekly(
val count: String val count: String
) )
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware { class MainViewModel(
override val di by closestDI(app) private val client: HttpClient
) : ViewModel() {
private val logger = newLogger(LoggerFactory.default)
private val client: HttpClient by instance()
val recentUpload = mutableStateListOf<Thumbnail>() val recentUpload = mutableStateListOf<Thumbnail>()
val mangaList = mutableStateListOf<Thumbnail>() val mangaList = mutableStateListOf<Thumbnail>()
val topWeekly = mutableStateListOf<TopWeekly>() val topWeekly = mutableStateListOf<TopWeekly>()
@@ -109,7 +106,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
topWeekly.add(TopWeekly(itemID, title, count)) topWeekly.add(TopWeekly(itemID, title, count))
} }
}.onFailure { }.onFailure {
logger.warning(it) TODO()
} }
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
@@ -37,11 +38,9 @@ import org.kodein.di.instance
import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
import xyz.quaver.pupil.sources.manatoki.manatokiUrl import xyz.quaver.pupil.sources.manatoki.manatokiUrl
class RecentViewModel(app: Application): AndroidViewModel(app), DIAware { class RecentViewModel(
override val di by closestDI(app) private val client: HttpClient
): ViewModel() {
private val client: HttpClient by instance()
var page by mutableStateOf(1) var page by mutableStateOf(1)
var loading by mutableStateOf(false) var loading by mutableStateOf(false)

View File

@@ -23,6 +23,7 @@ import android.os.Parcelable
import android.util.Log import android.util.Log
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -115,13 +116,11 @@ val availableSst = mapOf(
"as_bookmark" to "북마크순" "as_bookmark" to "북마크순"
) )
class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware { class SearchViewModel(
override val di by closestDI(app) private val client: HttpClient
) : ViewModel() {
private val logger = newLogger(LoggerFactory.default) private val logger = newLogger(LoggerFactory.default)
private val client: HttpClient by instance()
// 발행 // 발행
var publish by mutableStateOf("") var publish by mutableStateOf("")
// 초성 // 초성

View File

@@ -39,7 +39,6 @@ import xyz.quaver.graphics.subsampledimage.ImageSource
import xyz.quaver.graphics.subsampledimage.newBitmapRegionDecoder import xyz.quaver.graphics.subsampledimage.newBitmapRegionDecoder
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.inputStream import xyz.quaver.io.util.inputStream
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.sources.SourceEntries import xyz.quaver.pupil.sources.SourceEntries
import java.security.MessageDigest import java.security.MessageDigest
@@ -55,17 +54,6 @@ val JsonElement.content
fun DIAware.source(source: String) = lazy { direct.source(source) } fun DIAware.source(source: String) = lazy { direct.source(source) }
fun DirectDIAware.source(source: String) = instance<SourceEntries>().toMap()[source]!! fun DirectDIAware.source(source: String) = instance<SourceEntries>().toMap()[source]!!
fun DIAware.database() = lazy { direct.database() }
fun DirectDIAware.database() = instance<AppDatabase>()
fun View.hide() {
visibility = View.INVISIBLE
}
fun View.show() {
visibility = View.VISIBLE
}
class FileXImageSource(val file: FileX): ImageSource { class FileXImageSource(val file: FileX): ImageSource {
private val decoder by lazy { private val decoder by lazy {
file.inputStream()!!.use { file.inputStream()!!.use {