Decentralize database
This commit is contained in:
@@ -45,7 +45,6 @@ import okhttp3.Protocol
|
||||
import org.kodein.di.*
|
||||
import org.kodein.di.android.x.androidXModule
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.db.databaseModule
|
||||
import xyz.quaver.pupil.sources.sourceModule
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.util.*
|
||||
@@ -54,7 +53,6 @@ class Pupil : Application(), DIAware {
|
||||
|
||||
override val di: DI by DI.lazy {
|
||||
import(androidXModule(this@Pupil))
|
||||
import(databaseModule)
|
||||
import(sourceModule)
|
||||
|
||||
bind { singleton { NetworkCache(applicationContext) } }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -20,12 +20,8 @@ package xyz.quaver.pupil.sources.composable
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
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.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -79,25 +73,22 @@ import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
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.graphics.subsampledimage.*
|
||||
import xyz.quaver.graphics.subsampledimage.ScaleTypes.CENTER_INSIDE
|
||||
import xyz.quaver.graphics.subsampledimage.ImageSource
|
||||
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.pupil.R
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.proto.ReaderOptions
|
||||
import xyz.quaver.pupil.proto.settingsDataStore
|
||||
import xyz.quaver.pupil.ui.theme.Orange500
|
||||
import xyz.quaver.pupil.util.FileXImageSource
|
||||
import xyz.quaver.pupil.util.NetworkCache
|
||||
import xyz.quaver.pupil.util.activity
|
||||
import xyz.quaver.pupil.util.rememberFileXImageSource
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sign
|
||||
@@ -189,8 +180,6 @@ open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAwar
|
||||
|
||||
var fullscreen by mutableStateOf(false)
|
||||
|
||||
private val database: AppDatabase by instance()
|
||||
|
||||
var error by mutableStateOf(false)
|
||||
|
||||
var imageCount by mutableStateOf(0)
|
||||
|
||||
@@ -51,6 +51,7 @@ import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastFirstOrNull
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
@@ -67,7 +68,7 @@ private enum class NavigationIconState {
|
||||
ARROW
|
||||
}
|
||||
|
||||
open class SearchBaseViewModel<T>(app: Application) : AndroidViewModel(app) {
|
||||
open class SearchBaseViewModel<T> : ViewModel() {
|
||||
val searchResults = mutableStateListOf<T>()
|
||||
|
||||
var sortModeIndex by mutableStateOf(0)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -47,20 +46,22 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.room.Room
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||
import com.google.accompanist.insets.ui.Scaffold
|
||||
import com.google.accompanist.insets.ui.TopAppBar
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.*
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.android.subDI
|
||||
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.newLogger
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.proto.settingsDataStore
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
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.imageUrlFromImage
|
||||
import xyz.quaver.pupil.ui.theme.Orange500
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
|
||||
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 logger = newLogger(LoggerFactory.default)
|
||||
|
||||
private val database: AppDatabase by instance()
|
||||
private val bookmarkDao = database.bookmarkDao()
|
||||
|
||||
override val name: String = "hitomi.la"
|
||||
override val iconResID: Int = R.drawable.hitomi
|
||||
|
||||
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
||||
navigation(startDestination = "hitomi.la/search", route = name) {
|
||||
composable("hitomi.la/search") { Search(navController) }
|
||||
composable("hitomi.la/reader/{itemID}") { Reader(navController) }
|
||||
composable("hitomi.la/search") { withDI(di) { Search(navController) } }
|
||||
composable("hitomi.la/reader/{itemID}") { withDI(di) { Reader(navController) } }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Search(navController: NavController) {
|
||||
val model: HitomiSearchResultViewModel = viewModel()
|
||||
val database: AppDatabase by rememberInstance()
|
||||
val bookmarkDao = remember { database.bookmarkDao() }
|
||||
val model: HitomiSearchResultViewModel by rememberViewModel()
|
||||
val database: HitomiDatabase by rememberInstance()
|
||||
val favoritesDao = remember { database.favoritesDao() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val bookmarks by bookmarkDao.getAll(name).observeAsState()
|
||||
val bookmarkSet by derivedStateOf {
|
||||
bookmarks?.toSet() ?: emptySet()
|
||||
val favorites by favoritesDao.getAll().collectAsState(emptyList())
|
||||
val favoritesSet by derivedStateOf {
|
||||
Collections.unmodifiableSet(favorites.mapTo(mutableSetOf()) { it.item })
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -200,11 +205,11 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
items(model.searchResults) {
|
||||
DetailedSearchResult(
|
||||
it,
|
||||
bookmarks = bookmarkSet,
|
||||
onBookmarkToggle = {
|
||||
favorites = favoritesSet,
|
||||
onFavoriteToggle = {
|
||||
coroutineScope.launch {
|
||||
if (it in bookmarkSet) bookmarkDao.delete(name, it)
|
||||
else bookmarkDao.insert(name, it)
|
||||
if (it in favoritesSet) favoritesDao.delete(it)
|
||||
else favoritesDao.insert(it)
|
||||
}
|
||||
}
|
||||
) { result ->
|
||||
@@ -219,8 +224,8 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
fun Reader(navController: NavController) {
|
||||
val model: ReaderBaseViewModel = viewModel()
|
||||
|
||||
val database: AppDatabase by rememberInstance()
|
||||
val bookmarkDao = database.bookmarkDao()
|
||||
val database: HitomiDatabase by rememberInstance()
|
||||
val favoritesDao = remember { database.favoritesDao() }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
@@ -228,7 +233,7 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
|
||||
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) {
|
||||
runCatching {
|
||||
val galleryID = itemID!!.toInt()
|
||||
@@ -271,13 +276,13 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
IconButton(onClick = {
|
||||
itemID?.let {
|
||||
coroutineScope.launch {
|
||||
if (bookmark) bookmarkDao.delete(name, it)
|
||||
else bookmarkDao.insert(name, it)
|
||||
if (isFavorite) favoritesDao.delete(it)
|
||||
else favoritesDao.insert(it)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
if (isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = null,
|
||||
tint = Orange500
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
package xyz.quaver.pupil.sources.hitomi
|
||||
|
||||
import android.app.Application
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -29,12 +28,6 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
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.hitomi.lib.GalleryBlock
|
||||
import xyz.quaver.pupil.sources.hitomi.lib.doSearch
|
||||
@@ -43,16 +36,9 @@ import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class HitomiSearchResultViewModel(app: Application) : SearchBaseViewModel<HitomiSearchResult>(app), DIAware {
|
||||
override val di by closestDI(app)
|
||||
|
||||
private val logger = newLogger(LoggerFactory.default)
|
||||
|
||||
private val client: HttpClient by instance()
|
||||
|
||||
private val database: AppDatabase by instance()
|
||||
private val bookmarkDao = database.bookmarkDao()
|
||||
|
||||
class HitomiSearchResultViewModel(
|
||||
private val client: HttpClient
|
||||
) : SearchBaseViewModel<HitomiSearchResult>() {
|
||||
private var cachedQuery: String? = null
|
||||
private var cachedSortByPopularity: Boolean? = null
|
||||
private val cache = mutableListOf<Int>()
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
package xyz.quaver.pupil.sources.hitomi.composable
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -101,8 +100,8 @@ private fun String.wordCapitalize() : String {
|
||||
@Composable
|
||||
fun DetailedSearchResult(
|
||||
result: HitomiSearchResult,
|
||||
bookmarks: Set<String>,
|
||||
onBookmarkToggle: (String) -> Unit = { },
|
||||
favorites: Set<String>,
|
||||
onFavoriteToggle: (String) -> Unit = { },
|
||||
onClick: (HitomiSearchResult) -> Unit = { }
|
||||
) {
|
||||
val painter = rememberImagePainter(result.thumbnail)
|
||||
@@ -169,8 +168,8 @@ fun DetailedSearchResult(
|
||||
key(result.tags) {
|
||||
TagGroup(
|
||||
tags = result.tags,
|
||||
bookmarks,
|
||||
onBookmarkToggle = onBookmarkToggle
|
||||
favorites,
|
||||
onFavoriteToggle = onFavoriteToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -192,13 +191,13 @@ fun DetailedSearchResult(
|
||||
)
|
||||
|
||||
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,
|
||||
tint = Orange500,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable {
|
||||
onBookmarkToggle(result.itemID)
|
||||
onFavoriteToggle(result.itemID)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -210,20 +209,20 @@ fun DetailedSearchResult(
|
||||
@Composable
|
||||
fun TagGroup(
|
||||
tags: List<String>,
|
||||
bookmarks: Set<String>,
|
||||
onBookmarkToggle: (String) -> Unit = { }
|
||||
favorites: Set<String>,
|
||||
onFavoriteToggle: (String) -> Unit = { }
|
||||
) {
|
||||
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)) {
|
||||
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 ->
|
||||
TagChip(
|
||||
tag = tag,
|
||||
isFavorite = bookmarkedTagsInList.contains(tag),
|
||||
onFavoriteClick = onBookmarkToggle
|
||||
isFavorite = favoriteTagsInList.contains(tag),
|
||||
onFavoriteClick = onFavoriteToggle
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -32,16 +32,27 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import androidx.room.Room
|
||||
import org.kodein.di.DIAware
|
||||
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.frontend.defaultLogFrontend
|
||||
import org.kodein.log.newLogger
|
||||
import org.kodein.log.withShortPackageKeepLast
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.manatoki.composable.Main
|
||||
import xyz.quaver.pupil.sources.manatoki.composable.Reader
|
||||
import xyz.quaver.pupil.sources.manatoki.composable.Recent
|
||||
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(
|
||||
ExperimentalMaterialApi::class,
|
||||
@@ -50,19 +61,27 @@ import xyz.quaver.pupil.sources.manatoki.composable.Search
|
||||
ExperimentalComposeUiApi::class
|
||||
)
|
||||
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 iconResID = R.drawable.manatoki
|
||||
|
||||
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
||||
navigation(route = name, startDestination = "manatoki.net/") {
|
||||
composable("manatoki.net/") { Main(navController) }
|
||||
composable("manatoki.net/reader/{itemID}") { Reader(navController) }
|
||||
composable("manatoki.net/search") { Search(navController) }
|
||||
composable("manatoki.net/recent") { Recent(navController) }
|
||||
composable("manatoki.net/") { withDI(di) { Main(navController) } }
|
||||
composable("manatoki.net/reader/{itemID}") { withDI(di) { Reader(navController) } }
|
||||
composable("manatoki.net/search") { withDI(di) { Search(navController) } }
|
||||
composable("manatoki.net/recent") { withDI(di) { Recent(navController) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
package xyz.quaver.pupil.sources.manatoki.composable
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
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.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
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.TopAppBar
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.compose.rememberInstance
|
||||
import org.kodein.di.compose.rememberViewModel
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.proto.settingsDataStore
|
||||
import xyz.quaver.pupil.sources.composable.SourceSelectDialog
|
||||
@@ -62,7 +60,7 @@ import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun Main(navController: NavController) {
|
||||
val model: MainViewModel = viewModel()
|
||||
val model: MainViewModel by rememberViewModel()
|
||||
|
||||
val client: HttpClient by rememberInstance()
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -57,10 +56,11 @@ import io.ktor.client.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.compose.rememberInstance
|
||||
import org.kodein.di.compose.rememberViewModel
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.sources.composable.ReaderBase
|
||||
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.ReaderInfo
|
||||
import xyz.quaver.pupil.sources.manatoki.getItem
|
||||
@@ -79,8 +79,9 @@ fun Reader(navController: NavController) {
|
||||
|
||||
val client: HttpClient by rememberInstance()
|
||||
|
||||
val database: AppDatabase by rememberInstance()
|
||||
val bookmarkDao = database.bookmarkDao()
|
||||
val database: ManatokiDatabase by rememberInstance()
|
||||
val favoriteDao = remember { database.favoriteDao() }
|
||||
val bookmarkDao = remember { database.bookmarkDao() }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
@@ -98,7 +99,7 @@ fun Reader(navController: NavController) {
|
||||
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)
|
||||
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
|
||||
@@ -174,13 +175,13 @@ fun Reader(navController: NavController) {
|
||||
IconButton(onClick = {
|
||||
itemID?.let {
|
||||
coroutineScope.launch {
|
||||
if (bookmark) bookmarkDao.delete("manatoki.net", it)
|
||||
else bookmarkDao.insert("manatoki.net", it)
|
||||
if (isFavorite) favoriteDao.delete(it)
|
||||
else favoriteDao.insert(it)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
if (isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = null,
|
||||
tint = Orange500
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.google.accompanist.insets.ui.TopAppBar
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.compose.rememberInstance
|
||||
import org.kodein.di.compose.rememberViewModel
|
||||
import xyz.quaver.pupil.sources.composable.OverscrollPager
|
||||
import xyz.quaver.pupil.sources.manatoki.MangaListing
|
||||
import xyz.quaver.pupil.sources.manatoki.getItem
|
||||
@@ -51,7 +52,7 @@ import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun Recent(navController: NavController) {
|
||||
val model: RecentViewModel = viewModel()
|
||||
val model: RecentViewModel by rememberViewModel()
|
||||
|
||||
val client: HttpClient by rememberInstance()
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import com.google.accompanist.insets.ui.TopAppBar
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.launch
|
||||
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.ModalTopSheetState
|
||||
import xyz.quaver.pupil.sources.composable.OverscrollPager
|
||||
@@ -70,7 +71,7 @@ import xyz.quaver.pupil.sources.manatoki.viewmodel.*
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun Search(navController: NavController) {
|
||||
val model: SearchViewModel = viewModel()
|
||||
val model: SearchViewModel by rememberViewModel()
|
||||
|
||||
val client: HttpClient by rememberInstance()
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ suspend fun HttpClient.getItem(
|
||||
}.toString()
|
||||
|
||||
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 {
|
||||
it.attributes()
|
||||
.first { it.key.startsWith("data-") }
|
||||
|
||||
@@ -21,6 +21,7 @@ package xyz.quaver.pupil.sources.manatoki.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
@@ -46,13 +47,9 @@ data class TopWeekly(
|
||||
val count: String
|
||||
)
|
||||
|
||||
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
override val di by closestDI(app)
|
||||
|
||||
private val logger = newLogger(LoggerFactory.default)
|
||||
|
||||
private val client: HttpClient by instance()
|
||||
|
||||
class MainViewModel(
|
||||
private val client: HttpClient
|
||||
) : ViewModel() {
|
||||
val recentUpload = mutableStateListOf<Thumbnail>()
|
||||
val mangaList = mutableStateListOf<Thumbnail>()
|
||||
val topWeekly = mutableStateListOf<TopWeekly>()
|
||||
@@ -109,7 +106,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
topWeekly.add(TopWeekly(itemID, title, count))
|
||||
}
|
||||
}.onFailure {
|
||||
logger.warning(it)
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.ktor.client.*
|
||||
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.manatokiUrl
|
||||
|
||||
class RecentViewModel(app: Application): AndroidViewModel(app), DIAware {
|
||||
override val di by closestDI(app)
|
||||
|
||||
private val client: HttpClient by instance()
|
||||
|
||||
class RecentViewModel(
|
||||
private val client: HttpClient
|
||||
): ViewModel() {
|
||||
var page by mutableStateOf(1)
|
||||
|
||||
var loading by mutableStateOf(false)
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -115,13 +116,11 @@ val availableSst = mapOf(
|
||||
"as_bookmark" to "북마크순"
|
||||
)
|
||||
|
||||
class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
override val di by closestDI(app)
|
||||
|
||||
class SearchViewModel(
|
||||
private val client: HttpClient
|
||||
) : ViewModel() {
|
||||
private val logger = newLogger(LoggerFactory.default)
|
||||
|
||||
private val client: HttpClient by instance()
|
||||
|
||||
// 발행
|
||||
var publish by mutableStateOf("")
|
||||
// 초성
|
||||
|
||||
@@ -39,7 +39,6 @@ import xyz.quaver.graphics.subsampledimage.ImageSource
|
||||
import xyz.quaver.graphics.subsampledimage.newBitmapRegionDecoder
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.inputStream
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -55,17 +54,6 @@ val JsonElement.content
|
||||
fun DIAware.source(source: String) = lazy { direct.source(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 {
|
||||
private val decoder by lazy {
|
||||
file.inputStream()!!.use {
|
||||
|
||||
Reference in New Issue
Block a user