diff --git a/composeApp/schemas/top.kagg886.pmf.backend.database.AppDatabase/2.json b/composeApp/schemas/top.kagg886.pmf.backend.database.AppDatabase/2.json new file mode 100644 index 0000000..9a8224b --- /dev/null +++ b/composeApp/schemas/top.kagg886.pmf.backend.database.AppDatabase/2.json @@ -0,0 +1,163 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "44ff089e92f51e4fc0030929629d403d", + "entities": [ + { + "tableName": "IllustHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `illust` TEXT NOT NULL, `createTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "illust", + "columnName": "illust", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "NovelHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `novel` TEXT NOT NULL, `createTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "novel", + "columnName": "novel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "DownloadItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `illust` TEXT NOT NULL, `success` INTEGER NOT NULL, `progress` REAL NOT NULL, `createTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "illust", + "columnName": "illust", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "success", + "columnName": "success", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "SearchHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `initialSort` TEXT NOT NULL, `initialTarget` TEXT NOT NULL, `initialKeyWords` TEXT NOT NULL, `tab` TEXT NOT NULL, `createTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "initialSort", + "columnName": "initialSort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialTarget", + "columnName": "initialTarget", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialKeyWords", + "columnName": "initialKeyWords", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tab", + "columnName": "tab", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '44ff089e92f51e4fc0030929629d403d')" + ] + } +} \ No newline at end of file diff --git a/composeApp/schemas/top.kagg886.pmf.backend.database.AppDatabase/3.json b/composeApp/schemas/top.kagg886.pmf.backend.database.AppDatabase/3.json new file mode 100644 index 0000000..0b95ca1 --- /dev/null +++ b/composeApp/schemas/top.kagg886.pmf.backend.database.AppDatabase/3.json @@ -0,0 +1,163 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "44ff089e92f51e4fc0030929629d403d", + "entities": [ + { + "tableName": "IllustHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `illust` TEXT NOT NULL, `createTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "illust", + "columnName": "illust", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "NovelHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `novel` TEXT NOT NULL, `createTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "novel", + "columnName": "novel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "DownloadItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `illust` TEXT NOT NULL, `success` INTEGER NOT NULL, `progress` REAL NOT NULL, `createTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "illust", + "columnName": "illust", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "success", + "columnName": "success", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "SearchHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `initialSort` TEXT NOT NULL, `initialTarget` TEXT NOT NULL, `initialKeyWords` TEXT NOT NULL, `tab` TEXT NOT NULL, `createTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "initialSort", + "columnName": "initialSort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialTarget", + "columnName": "initialTarget", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialKeyWords", + "columnName": "initialKeyWords", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tab", + "columnName": "tab", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '44ff089e92f51e4fc0030929629d403d')" + ] + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/App.kt b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/App.kt index bb64c72..015e6c4 100644 --- a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/App.kt +++ b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/App.kt @@ -260,6 +260,8 @@ fun startKoin0() { module(createdAtStart = true) { single { getDataBaseBuilder() + .fallbackToDestructiveMigrationOnDowngrade(true) + .fallbackToDestructiveMigration(true) .fallbackToDestructiveMigrationFrom(true,1) .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) diff --git a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/illust-history.kt b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/illust-history.kt index a668e09..bdd86f3 100644 --- a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/illust-history.kt +++ b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/illust-history.kt @@ -9,7 +9,7 @@ interface IllustHistoryDAO { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(item: IllustHistory) - @Query("SELECT * FROM IllustHistory ORDER BY createTime DESC LIMIT :size OFFSET (:page - 1) * 30;") + @Query("SELECT * FROM IllustHistory ORDER BY createTime DESC LIMIT :size OFFSET (:page - 1) * :size;") suspend fun getByPage(page: Int = 1, size: Int = 30): List } diff --git a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/novel-history.kt b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/novel-history.kt index 50b6f96..a9bc48f 100644 --- a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/novel-history.kt +++ b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/novel-history.kt @@ -9,7 +9,7 @@ interface NovelHistoryDAO { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(item: NovelHistory) - @Query("SELECT * FROM NovelHistory ORDER BY createTime DESC LIMIT :size OFFSET (:page - 1) * 30;") + @Query("SELECT * FROM NovelHistory ORDER BY createTime DESC LIMIT :size OFFSET (:page - 1) * :size;") suspend fun getByPage(page: Int = 1, size: Int = 30): List } diff --git a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/search-history.kt b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/search-history.kt new file mode 100644 index 0000000..8d83718 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/dao/search-history.kt @@ -0,0 +1,32 @@ +package top.kagg886.pmf.backend.database.dao + +import androidx.room.* +import kotlinx.coroutines.flow.Flow +import top.kagg886.pixko.module.search.SearchSort +import top.kagg886.pixko.module.search.SearchTarget +import top.kagg886.pmf.ui.route.main.search.SearchTab + + +@Dao +interface SearchHistoryDAO { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(item: SearchHistory) + + + @Query("SELECT * FROM SearchHistory ORDER BY createTime DESC") + fun allFlow(): Flow> + + @Delete + suspend fun delete(item: SearchHistory) +} + +@Entity +data class SearchHistory( + @PrimaryKey(autoGenerate = true) + val id: Long, + val initialSort:SearchSort, + val initialTarget:SearchTarget, + val initialKeyWords:String, + val tab:SearchTab, + val createTime: Long = System.currentTimeMillis() +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/database.kt b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/database.kt index 885864f..acb0abd 100644 --- a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/database.kt +++ b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/backend/database/database.kt @@ -7,14 +7,15 @@ import androidx.room.RoomDatabaseConstructor import top.kagg886.pmf.backend.database.dao.* @Database( - entities = [IllustHistory::class, NovelHistory::class, DownloadItem::class], - version = 2 + entities = [IllustHistory::class, NovelHistory::class, DownloadItem::class, SearchHistory::class], + version = 3 ) @ConstructedBy(AppDatabaseConstructor::class) abstract class AppDatabase : RoomDatabase() { abstract fun illustHistoryDAO(): IllustHistoryDAO abstract fun novelHistoryDAO(): NovelHistoryDAO abstract fun downloadDAO(): DownloadDao + abstract fun searchHistoryDAO(): SearchHistoryDAO } // The Room compiler generates the `actual` implementations. diff --git a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchScreen.kt index 5ad22c2..5733b68 100644 --- a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchScreen.kt @@ -4,8 +4,10 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* @@ -45,6 +47,11 @@ class SearchScreen( private val initialKeyWords: String = "", val tab: SearchTab = SearchTab.ILLUST ) : Screen { + private class PageScreenModel( + val page: MutableState = mutableIntStateOf(0) + ) : ScreenModel + + private var illust_dirty = -1 private var novel_dirty = -1 @@ -66,37 +73,42 @@ class SearchScreen( fun SearchScreenContent(state: SearchViewState) { val nav = LocalNavigator.currentOrThrow val model = nav.koinNavigatorScreenModel() + + var active by remember { mutableStateOf(false) } val padding by animateDpAsState(if (active) 0.dp else 16.dp) - var sort by mutableStateOf(initialSort) - var target by mutableStateOf(initialTarget) + var sort by remember { mutableStateOf(initialSort) } + var target by remember { mutableStateOf(initialTarget) } var keyWords by remember { mutableStateOf(initialKeyWords) } var searchWords by remember { mutableStateOf(initialKeyWords) } - LaunchedEffect(Unit) { snapshotFlow { keyWords }.debounce(1.seconds).distinctUntilChanged().collectLatest { + if (keyWords.isEmpty()) { + searchWords = "" + } model.searchTag(keyWords.split(" ").last()) } } - fun startSearch() { if (keyWords.isEmpty()) { return } + with(Random(System.currentTimeMillis())) { illust_dirty = nextInt() novel_dirty = nextInt() } searchWords = keyWords active = false + model.saveSearchHistory(sort, target, searchWords, tab) } Column(modifier = Modifier.fillMaxSize()) { SearchBar( query = keyWords, onQueryChange = { keyWords = it }, - onSearch = { }, + onSearch = { startSearch() }, active = active, onActiveChange = { active = it }, placeholder = { Text("搜索") }, @@ -200,6 +212,9 @@ class SearchScreen( for (tag in state.tag) { AssistChip( onClick = { + model.saveSearchHistory( + sort, target, tag.tag.name, SearchTab.ILLUST + ) nav.push( SearchScreen( sort, target, tag.tag.name @@ -301,17 +316,83 @@ class SearchScreen( } return } - + if (state is CanAccessHistory) { + val list by state.history.collectAsState(initial = listOf()) + if (list.isNotEmpty()) { + LazyColumn { + items(list) { + ListItem( + overlineContent = { + Text(it.initialKeyWords) + }, + trailingContent = { + IconButton( + onClick = { + model.deleteSearchHistory(it) + } + ) { + Icon(imageVector = Icons.Default.Delete, contentDescription = "") + } + }, + headlineContent = { + FlowRow { + SuggestionChip( + onClick = {}, + label = { + Text( + when (it.initialSort) { + DATE_DESC -> "时间倒序" + DATE_ASC -> "时间正序" + POPULAR_DESC -> "热门倒序" + } + ) + }, + modifier = Modifier.padding(start = 8.dp) + ) + SuggestionChip( + onClick = {}, + label = { + Text( + when (it.initialTarget) { + PARTIAL_MATCH_FOR_TAGS -> "部分匹配" + EXACT_MATCH_FOR_TAGS -> "精确匹配" + TITLE_AND_CAPTION -> "标题简介" + } + ) + }, + modifier = Modifier.padding(start = 8.dp) + ) + SuggestionChip( + onClick = {}, + label = { + Text(it.tab.display) + }, + modifier = Modifier.padding(start = 8.dp) + ) + } + }, + modifier = Modifier.clickable { + nav.push( + SearchScreen( + initialSort = it.initialSort, + initialTarget = it.initialTarget, + initialKeyWords = it.initialKeyWords, + tab = it.tab + ) + ) + } + ) + } + } + return + } + } Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("点击搜索框以进行搜索") - Text("快捷TAG放在了搜索框中") + Text("暂无历史记录") + Text("点击搜索框进行一次搜索吧!") } } } } - - private class PageScreenModel( - val page: MutableState = mutableIntStateOf(0) - ) : ScreenModel } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchViewModel.kt b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchViewModel.kt index 38d597c..f1be13e 100644 --- a/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchViewModel.kt +++ b/composeApp/src/commonMain/kotlin/top/kagg886/pmf/ui/route/main/search/SearchViewModel.kt @@ -2,27 +2,35 @@ package top.kagg886.pmf.ui.route.main.search import androidx.lifecycle.ViewModel import cafe.adriel.voyager.core.model.ScreenModel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.runBlocking import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost -import top.kagg886.pixko.PixivAccountFactory import top.kagg886.pixko.Tag +import top.kagg886.pixko.module.search.SearchSort +import top.kagg886.pixko.module.search.SearchTarget import top.kagg886.pixko.module.search.searchTag import top.kagg886.pixko.module.trending.TrendingTags import top.kagg886.pixko.module.trending.getRecommendTags +import top.kagg886.pmf.backend.database.AppDatabase +import top.kagg886.pmf.backend.database.dao.NovelHistory +import top.kagg886.pmf.backend.database.dao.SearchHistory import top.kagg886.pmf.backend.pixiv.PixivConfig -import top.kagg886.pmf.backend.pixiv.PixivTokenStorage import top.kagg886.pmf.ui.util.container -class SearchViewModel : ContainerHost, ViewModel(), ScreenModel { +class SearchViewModel : ContainerHost, ViewModel(), ScreenModel, KoinComponent { private val client = PixivConfig.newAccountFromConfig() + private val database by inject() + + private val history = database.searchHistoryDAO().allFlow() + override val container: Container = container(SearchViewState.NonLoading) { val tag = client.getRecommendTags() reduce { - SearchViewState.EmptySearch(tag) + SearchViewState.EmptySearch(tag,history) } } private val tagCache by lazy { @@ -30,25 +38,51 @@ class SearchViewModel : ContainerHost, ViewModel(), Sc client.getRecommendTags() } } + fun searchTag(key: String) = intent { reduce { SearchViewState.NonLoading } if (key.isBlank()) { reduce { - SearchViewState.EmptySearch(tagCache) + SearchViewState.EmptySearch(tagCache,history) } return@intent } val t = client.searchTag(key) reduce { - SearchViewState.KeyWordSearch(t) + SearchViewState.KeyWordSearch(t,history) } } + + fun saveSearchHistory( + sort: SearchSort, + target: SearchTarget, + key: String, + tab: SearchTab + ) = intent { + database.searchHistoryDAO().insert( + SearchHistory( + -1, + sort, + target, + key, + tab + ) + ) + } + + fun deleteSearchHistory(history: SearchHistory) = intent { + database.searchHistoryDAO().delete(history) + } +} + +sealed interface SearchViewState { + data object NonLoading : SearchViewState + data class EmptySearch(val tag: List, override val history: Flow>) : SearchViewState,CanAccessHistory + data class KeyWordSearch(val key: List, override val history: Flow>) : SearchViewState,CanAccessHistory } -sealed class SearchViewState { - data object NonLoading : SearchViewState() - data class EmptySearch(val tag: List) : SearchViewState() - data class KeyWordSearch(val key: List) : SearchViewState() +interface CanAccessHistory { + val history: Flow> } \ No newline at end of file