diff --git a/app/schemas/com.jerboa.db.AppDB/21.json b/app/schemas/com.jerboa.db.AppDB/21.json new file mode 100644 index 000000000..4dd230129 --- /dev/null +++ b/app/schemas/com.jerboa.db.AppDB/21.json @@ -0,0 +1,272 @@ +{ + "formatVersion": 1, + "database": { + "version": 21, + "identityHash": "a477af5ed41b35ad0a3693e0b276cff7", + "entities": [ + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `current` INTEGER NOT NULL, `instance` TEXT NOT NULL, `name` TEXT NOT NULL, `jwt` TEXT NOT NULL, `default_listing_type` INTEGER NOT NULL DEFAULT 0, `default_sort_type` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jwt", + "columnName": "jwt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultListingType", + "columnName": "default_listing_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "defaultSortType", + "columnName": "default_sort_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AppSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `font_size` INTEGER NOT NULL DEFAULT 16, `theme` INTEGER NOT NULL DEFAULT 0, `theme_color` INTEGER NOT NULL DEFAULT 0, `viewed_changelog` INTEGER NOT NULL DEFAULT 0, `post_view_mode` INTEGER NOT NULL DEFAULT 0, `show_bottom_nav` INTEGER NOT NULL DEFAULT 1, `show_collapsed_comment_content` INTEGER NOT NULL DEFAULT 0, `show_comment_action_bar_by_default` INTEGER NOT NULL DEFAULT 1, `show_voting_arrows_in_list_view` INTEGER NOT NULL DEFAULT 1, `show_parent_comment_navigation_buttons` INTEGER NOT NULL DEFAULT 0, `navigate_parent_comments_with_volume_buttons` INTEGER NOT NULL DEFAULT 0, `use_custom_tabs` INTEGER NOT NULL DEFAULT 1, `use_private_tabs` INTEGER NOT NULL DEFAULT 0, `secure_window` INTEGER NOT NULL DEFAULT 0, `blur_nsfw` INTEGER NOT NULL DEFAULT 1, `show_text_descriptions_in_navbar` INTEGER NOT NULL DEFAULT 1, `backConfirmationMode` INTEGER NOT NULL DEFAULT 1, `save_search_history` INTEGER NOT NULL DEFAULT 1)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontSize", + "columnName": "font_size", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "16" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "themeColor", + "columnName": "theme_color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "viewedChangelog", + "columnName": "viewed_changelog", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "postViewMode", + "columnName": "post_view_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showBottomNav", + "columnName": "show_bottom_nav", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showCollapsedCommentContent", + "columnName": "show_collapsed_comment_content", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showCommentActionBarByDefault", + "columnName": "show_comment_action_bar_by_default", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showVotingArrowsInListView", + "columnName": "show_voting_arrows_in_list_view", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showParentCommentNavigationButtons", + "columnName": "show_parent_comment_navigation_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "navigateParentCommentsWithVolumeButtons", + "columnName": "navigate_parent_comments_with_volume_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "useCustomTabs", + "columnName": "use_custom_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "usePrivateTabs", + "columnName": "use_private_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "secureWindow", + "columnName": "secure_window", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blurNSFW", + "columnName": "blur_nsfw", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showTextDescriptionsInNavbar", + "columnName": "show_text_descriptions_in_navbar", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "backConfirmationMode", + "columnName": "backConfirmationMode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "saveSearchHistory", + "columnName": "save_search_history", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SearchHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER, `search_term` TEXT NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "account_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchTerm", + "columnName": "search_term", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchHistory_account_id", + "unique": false, + "columnNames": [ + "account_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SearchHistory_account_id` ON `${TABLE_NAME}` (`account_id`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "account_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "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, 'a477af5ed41b35ad0a3693e0b276cff7')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt index eac975752..8462396fa 100644 --- a/app/src/main/java/com/jerboa/MainActivity.kt +++ b/app/src/main/java/com/jerboa/MainActivity.kt @@ -47,6 +47,8 @@ import com.jerboa.model.AppSettingsViewModel import com.jerboa.model.AppSettingsViewModelFactory import com.jerboa.model.CommunityViewModel import com.jerboa.model.ReplyItem +import com.jerboa.model.SearchHistoryViewModel +import com.jerboa.model.SearchHistoryViewModelFactory import com.jerboa.model.SiteViewModel import com.jerboa.ui.components.comment.edit.CommentEditActivity import com.jerboa.ui.components.comment.reply.CommentReplyActivity @@ -111,6 +113,7 @@ class MainActivity : AppCompatActivity() { val accountViewModel: AccountViewModel = viewModel(factory = AccountViewModelFactory.Factory) val appSettingsViewModel: AppSettingsViewModel = viewModel(factory = AppSettingsViewModelFactory.Factory) val accountSettingsViewModel: AccountSettingsViewModel = viewModel(factory = AccountSettingsViewModelFactory.Factory) + val searchHistoryViewModel: SearchHistoryViewModel = viewModel(factory = SearchHistoryViewModelFactory.Factory) API.errorHandler = { Log.e("jerboa", it.toString()) @@ -239,6 +242,7 @@ class MainActivity : AppCompatActivity() { accountViewModel = accountViewModel, siteViewModel = siteViewModel, appSettingsViewModel = appSettingsViewModel, + searchHistoryViewModel = searchHistoryViewModel, appSettings = appSettings, drawerState = drawerState, ) @@ -421,10 +425,12 @@ class MainActivity : AppCompatActivity() { CommunityListActivity( navController = navController, accountViewModel = accountViewModel, - siteViewModel = siteViewModel, selectMode = args.select, + siteViewModel = siteViewModel, blurNSFW = appSettings.blurNSFW, drawerState = drawerState, + appSettingsViewModel = appSettingsViewModel, + searchHistoryViewModel = searchHistoryViewModel, ) } diff --git a/app/src/main/java/com/jerboa/db/AppDB.kt b/app/src/main/java/com/jerboa/db/AppDB.kt index dd63bb183..374f948e9 100644 --- a/app/src/main/java/com/jerboa/db/AppDB.kt +++ b/app/src/main/java/com/jerboa/db/AppDB.kt @@ -9,8 +9,10 @@ import androidx.room.RoomDatabase import androidx.sqlite.db.SupportSQLiteDatabase import com.jerboa.db.dao.AccountDao import com.jerboa.db.dao.AppSettingsDao +import com.jerboa.db.dao.SearchHistoryDao import com.jerboa.db.entity.Account import com.jerboa.db.entity.AppSettings +import com.jerboa.db.entity.SearchHistory import java.util.concurrent.Executors const val DEFAULT_FONT_SIZE = 16 @@ -34,16 +36,18 @@ val APP_SETTINGS_DEFAULT = AppSettings( blurNSFW = true, showTextDescriptionsInNavbar = true, backConfirmationMode = 1, + saveSearchHistory = true, ) @Database( - version = 20, - entities = [Account::class, AppSettings::class], + version = 21, + entities = [Account::class, AppSettings::class, SearchHistory::class], exportSchema = true, ) abstract class AppDB : RoomDatabase() { abstract fun accountDao(): AccountDao abstract fun appSettingsDao(): AppSettingsDao + abstract fun searchHistoryDao(): SearchHistoryDao companion object { @Volatile diff --git a/app/src/main/java/com/jerboa/db/AppDBContainer.kt b/app/src/main/java/com/jerboa/db/AppDBContainer.kt index c3949664f..aee43ad34 100644 --- a/app/src/main/java/com/jerboa/db/AppDBContainer.kt +++ b/app/src/main/java/com/jerboa/db/AppDBContainer.kt @@ -3,9 +3,13 @@ package com.jerboa.db import android.content.Context import com.jerboa.db.repository.AccountRepository import com.jerboa.db.repository.AppSettingsRepository +import com.jerboa.db.repository.SearchHistoryRepository class AppDBContainer(private val context: Context) { private val database by lazy { AppDB.getDatabase(context) } val accountRepository by lazy { AccountRepository(database.accountDao()) } - val appSettingsRepository by lazy { AppSettingsRepository(database.appSettingsDao()) } + val appSettingsRepository by lazy { + AppSettingsRepository(database.appSettingsDao(), database.searchHistoryDao()) + } + val searchHistoryRepository by lazy { SearchHistoryRepository(database.searchHistoryDao()) } } diff --git a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt index 347f494b7..1751fc9a1 100644 --- a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt +++ b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt @@ -261,6 +261,31 @@ val MIGRATION_19_20 = object : Migration(19, 20) { } } +val MIGRATION_20_21 = object : Migration(20, 21) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL(UPDATE_APP_CHANGELOG_UNVIEWED) + database.execSQL( + "ALTER TABLE AppSettings ADD COLUMN save_search_history INTEGER NOT NULL DEFAULT 1", + ) + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS SearchHistory( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + account_id INTEGER, + search_term TEXT NOT NULL, + FOREIGN KEY(account_id) + REFERENCES Account(id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) + """, + ) + database.execSQL( + "CREATE INDEX index_SearchHistory_account_id ON SearchHistory(account_id)", + ) + } +} + // Don't forget to test your migration with `./gradlew app:connectAndroidTest` val MIGRATIONS_LIST = arrayOf( MIGRATION_1_2, @@ -282,4 +307,5 @@ val MIGRATIONS_LIST = arrayOf( MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20, + MIGRATION_20_21, ) diff --git a/app/src/main/java/com/jerboa/db/dao/SearchHistoryDao.kt b/app/src/main/java/com/jerboa/db/dao/SearchHistoryDao.kt new file mode 100644 index 000000000..5ddf69fda --- /dev/null +++ b/app/src/main/java/com/jerboa/db/dao/SearchHistoryDao.kt @@ -0,0 +1,23 @@ +package com.jerboa.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.jerboa.db.entity.SearchHistory + +@Dao +interface SearchHistoryDao { + @Query("SELECT * FROM SearchHistory") + fun history(): LiveData> + + @Insert(onConflict = OnConflictStrategy.REPLACE, entity = SearchHistory::class) + suspend fun insert(item: SearchHistory) + + @Query("DELETE FROM SearchHistory WHERE account_id IS :accountId AND search_term = :searchTerm") + suspend fun delete(accountId: Int?, searchTerm: String) + + @Query("DELETE FROM SearchHistory") + suspend fun clear() +} diff --git a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt index e0aaba3e0..d3f4d1527 100644 --- a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt +++ b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt @@ -93,4 +93,9 @@ data class AppSettings( defaultValue = "1", ) val backConfirmationMode: Int, + @ColumnInfo( + name = "save_search_history", + defaultValue = "1", + ) + val saveSearchHistory: Boolean, ) diff --git a/app/src/main/java/com/jerboa/db/entity/SearchHistory.kt b/app/src/main/java/com/jerboa/db/entity/SearchHistory.kt new file mode 100644 index 000000000..38bc9630a --- /dev/null +++ b/app/src/main/java/com/jerboa/db/entity/SearchHistory.kt @@ -0,0 +1,29 @@ +package com.jerboa.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = Account::class, + parentColumns = ["id"], + childColumns = ["account_id"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE, + ), + ], +) +data class SearchHistory( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + @ColumnInfo( + name = "account_id", + index = true, + ) + val accountId: Int?, + @ColumnInfo(name = "search_term") + val searchTerm: String, +) diff --git a/app/src/main/java/com/jerboa/db/repository/AppSettingsRepository.kt b/app/src/main/java/com/jerboa/db/repository/AppSettingsRepository.kt index f3626c29d..28e775853 100644 --- a/app/src/main/java/com/jerboa/db/repository/AppSettingsRepository.kt +++ b/app/src/main/java/com/jerboa/db/repository/AppSettingsRepository.kt @@ -3,6 +3,7 @@ package com.jerboa.db.repository import android.util.Log import androidx.annotation.WorkerThread import com.jerboa.db.dao.AppSettingsDao +import com.jerboa.db.dao.SearchHistoryDao import com.jerboa.db.entity.AppSettings import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -17,6 +18,7 @@ import okhttp3.Request // instead of the whole database, because you only need access to the DAO class AppSettingsRepository( private val appSettingsDao: AppSettingsDao, + private val searchHistoryDao: SearchHistoryDao, private val httpClient: OkHttpClient = OkHttpClient(), private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { @@ -31,6 +33,9 @@ class AppSettingsRepository( @WorkerThread suspend fun update(appSettings: AppSettings) { appSettingsDao.updateAppSettings(appSettings) + if (!appSettings.saveSearchHistory) { + searchHistoryDao.clear() + } } @WorkerThread diff --git a/app/src/main/java/com/jerboa/db/repository/SearchHistoryRepository.kt b/app/src/main/java/com/jerboa/db/repository/SearchHistoryRepository.kt new file mode 100644 index 000000000..c27433649 --- /dev/null +++ b/app/src/main/java/com/jerboa/db/repository/SearchHistoryRepository.kt @@ -0,0 +1,22 @@ +package com.jerboa.db.repository + +import androidx.lifecycle.LiveData +import androidx.lifecycle.map +import com.jerboa.db.dao.SearchHistoryDao +import com.jerboa.db.entity.SearchHistory + +class SearchHistoryRepository( + private val searchHistoryDao: SearchHistoryDao, +) { + fun history(): LiveData> = searchHistoryDao.history() + .map { history -> + history + .sortedByDescending { it.id } + .distinctBy { it.searchTerm } + } + + suspend fun insert(item: SearchHistory) = + searchHistoryDao.insert(item) + + suspend fun delete(item: SearchHistory) = searchHistoryDao.delete(item.accountId, item.searchTerm) +} diff --git a/app/src/main/java/com/jerboa/model/CommunityListViewModel.kt b/app/src/main/java/com/jerboa/model/CommunityListViewModel.kt index 4c88d67b4..9ec8334c5 100644 --- a/app/src/main/java/com/jerboa/model/CommunityListViewModel.kt +++ b/app/src/main/java/com/jerboa/model/CommunityListViewModel.kt @@ -12,7 +12,6 @@ import com.jerboa.datatypes.types.CommunityAggregates import com.jerboa.datatypes.types.CommunityView import com.jerboa.datatypes.types.Search import com.jerboa.datatypes.types.SearchResponse -import com.jerboa.datatypes.types.SearchType import com.jerboa.datatypes.types.SubscribedType import com.jerboa.serializeToMap import com.jerboa.ui.components.common.Initializable @@ -24,6 +23,8 @@ class CommunityListViewModel : ViewModel(), Initializable { var searchRes: ApiState by mutableStateOf(ApiState.Empty) private set + var communities: List by mutableStateOf(emptyList()) + fun searchCommunities(form: Search) { viewModelScope.launch { searchRes = ApiState.Loading @@ -38,7 +39,7 @@ class CommunityListViewModel : ViewModel(), Initializable { val myFollows = myUserInfo.follows // A hack to convert communityFollowerView into CommunityView - val followsIntoCommunityViews = myFollows.map { cfv -> + communities = myFollows.map { cfv -> CommunityView( community = cfv.community, subscribed = SubscribedType.Subscribed, @@ -57,20 +58,14 @@ class CommunityListViewModel : ViewModel(), Initializable { ), ) } - - searchRes = ApiState.Success( - SearchResponse( - type_ = SearchType.Communities, - communities = followsIntoCommunityViews, - comments = emptyList(), - posts = emptyList(), - users = emptyList(), - ), - ) } } else -> {} } } + + fun resetSearch() { + searchRes = ApiState.Empty + } } diff --git a/app/src/main/java/com/jerboa/model/SearchHistoryViewModel.kt b/app/src/main/java/com/jerboa/model/SearchHistoryViewModel.kt new file mode 100644 index 000000000..52fbf2d91 --- /dev/null +++ b/app/src/main/java/com/jerboa/model/SearchHistoryViewModel.kt @@ -0,0 +1,28 @@ +package com.jerboa.model + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.jerboa.db.entity.SearchHistory +import com.jerboa.db.repository.SearchHistoryRepository +import com.jerboa.jerboaApplication + +class SearchHistoryViewModel(private val repository: SearchHistoryRepository) : ViewModel() { + val searchHistory = repository.history() + + suspend fun insert(item: SearchHistory) { + repository.insert(item) + } + + suspend fun delete(item: SearchHistory) { + repository.delete(item) + } +} + +object SearchHistoryViewModelFactory { + val Factory = viewModelFactory { + initializer { + SearchHistoryViewModel(jerboaApplication().container.searchHistoryRepository) + } + } +} diff --git a/app/src/main/java/com/jerboa/ui/components/community/list/CommunityList.kt b/app/src/main/java/com/jerboa/ui/components/community/list/CommunityList.kt index 2a452e8da..a8acafae0 100644 --- a/app/src/main/java/com/jerboa/ui/components/community/list/CommunityList.kt +++ b/app/src/main/java/com/jerboa/ui/components/community/list/CommunityList.kt @@ -1,5 +1,7 @@ package com.jerboa.ui.components.community.list +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -7,9 +9,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* +import androidx.compose.material.icons.rounded.Close import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField @@ -24,6 +28,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.jerboa.R import com.jerboa.datatypes.sampleCommunityView import com.jerboa.datatypes.types.* +import com.jerboa.db.entity.SearchHistory import com.jerboa.ui.components.common.simpleVerticalScrollbar import com.jerboa.ui.components.community.CommunityLinkLarger import com.jerboa.ui.components.community.CommunityLinkLargerWithUserCount @@ -161,3 +166,51 @@ fun SearchViewPreview() { onSearchChange = {}, ) } + +@Composable +fun SearchHistoryList( + history: List, + onHistoryItemClicked: (SearchHistory) -> Unit, + onHistoryItemDeleted: (SearchHistory) -> Unit, +) { + Column { + if (history.isNotEmpty()) { + ListItem( + headlineContent = { + Text( + text = stringResource(R.string.community_list_recent_searches), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelLarge, + ) + }, + ) + } + history.forEach { + ListItem( + modifier = Modifier.clickable { onHistoryItemClicked(it) }, + headlineContent = { + Text( + text = it.searchTerm, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + }, + trailingContent = { + IconButton( + onClick = { onHistoryItemDeleted(it) }, + content = { + Icon( + Icons.Rounded.Close, + contentDescription = stringResource( + R.string.community_list_delete_search_item, + it.searchTerm, + ), + tint = MaterialTheme.colorScheme.surfaceTint, + ) + }, + ) + }, + ) + } + } +} diff --git a/app/src/main/java/com/jerboa/ui/components/community/list/CommunityListActivity.kt b/app/src/main/java/com/jerboa/ui/components/community/list/CommunityListActivity.kt index 1540590c2..f703fa916 100644 --- a/app/src/main/java/com/jerboa/ui/components/community/list/CommunityListActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/community/list/CommunityListActivity.kt @@ -1,30 +1,40 @@ package com.jerboa.ui.components.community.list import android.util.Log +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.DrawerState +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.jerboa.DEBOUNCE_DELAY +import com.jerboa.R import com.jerboa.api.ApiState import com.jerboa.datatypes.types.Search import com.jerboa.datatypes.types.SearchType import com.jerboa.datatypes.types.SortType +import com.jerboa.db.entity.SearchHistory import com.jerboa.model.AccountViewModel +import com.jerboa.model.AppSettingsViewModel import com.jerboa.model.CommunityListViewModel +import com.jerboa.model.SearchHistoryViewModel import com.jerboa.model.SiteViewModel -import com.jerboa.ui.components.common.ApiEmptyText import com.jerboa.ui.components.common.ApiErrorText import com.jerboa.ui.components.common.InitializeRoute import com.jerboa.ui.components.common.LoadingBar @@ -35,8 +45,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -private var fetchCommunitiesJob: Job? = null - object CommunityListReturn { const val COMMUNITY = "community-list::return(community)" } @@ -49,6 +57,8 @@ fun CommunityListActivity( siteViewModel: SiteViewModel, blurNSFW: Boolean, drawerState: DrawerState, + appSettingsViewModel: AppSettingsViewModel, + searchHistoryViewModel: SearchHistoryViewModel, ) { Log.d("jerboa", "got to community list activity") @@ -60,9 +70,20 @@ fun CommunityListActivity( communityListViewModel.setCommunityListFromFollowed(siteViewModel) } + val saveSearchHistory by remember { + appSettingsViewModel.appSettings + .map { it.saveSearchHistory } + }.observeAsState() + + val searchHistory by remember(account) { + searchHistoryViewModel.searchHistory + .map { history -> history.filter { it.accountId == account?.id } } + }.observeAsState() + var search by rememberSaveable { mutableStateOf("") } val scope = rememberCoroutineScope() + var fetchCommunitiesJob by remember { mutableStateOf(null) } Surface(color = MaterialTheme.colorScheme.background) { Scaffold( @@ -76,6 +97,10 @@ fun CommunityListActivity( search = search, onSearchChange = { search = it + if (search.isEmpty()) { + communityListViewModel.resetSearch() + return@CommunityListHeader + } fetchCommunitiesJob?.cancel() fetchCommunitiesJob = scope.launch { delay(DEBOUNCE_DELAY) @@ -87,13 +112,83 @@ fun CommunityListActivity( auth = account?.jwt, ), ) + if (saveSearchHistory == true) { + searchHistoryViewModel.insert( + SearchHistory( + accountId = account?.id, + searchTerm = search.trim(), + ), + ) + } } }, ) }, content = { padding -> when (val communitiesRes = communityListViewModel.searchRes) { - ApiState.Empty -> ApiEmptyText() + ApiState.Empty -> { + Column( + modifier = Modifier + .padding(padding) + .imePadding(), + ) { + searchHistory?.let { history -> + SearchHistoryList( + history = history, + onHistoryItemClicked = { + search = it.searchTerm + fetchCommunitiesJob?.cancel() + fetchCommunitiesJob = scope.launch { + communityListViewModel.searchCommunities( + Search( + q = it.searchTerm, + type_ = SearchType.Communities, + sort = SortType.TopAll, + auth = account?.jwt, + ), + ) + if (saveSearchHistory == true) { + searchHistoryViewModel.insert( + SearchHistory( + accountId = account?.id, + searchTerm = search.trim(), + ), + ) + } + } + }, + onHistoryItemDeleted = { + scope.launch { + searchHistoryViewModel.delete(it) + } + }, + ) + } + ListItem( + headlineContent = { + Text( + text = stringResource(R.string.community_list_title), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelLarge, + ) + }, + ) + CommunityListings( + communities = communityListViewModel.communities, + onClickCommunity = { cs -> + if (selectMode) { + navController.apply { + addReturn(CommunityListReturn.COMMUNITY, cs) + navigateUp() + } + } else { + navController.toCommunity(id = cs.id) + } + }, + blurNSFW = blurNSFW, + ) + } + } is ApiState.Failure -> ApiErrorText(communitiesRes.msg) ApiState.Loading -> { LoadingBar(padding) diff --git a/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt b/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt index 530d7481b..a9efe25fa 100644 --- a/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt @@ -46,6 +46,7 @@ import com.jerboa.loginFirstToast import com.jerboa.model.AccountViewModel import com.jerboa.model.AppSettingsViewModel import com.jerboa.model.HomeViewModel +import com.jerboa.model.SearchHistoryViewModel import com.jerboa.model.SiteViewModel import com.jerboa.ui.components.common.BottomAppBarAll import com.jerboa.ui.components.common.InitializeRoute @@ -84,6 +85,7 @@ fun BottomNavActivity( appSettingsViewModel: AppSettingsViewModel, appSettings: AppSettings, drawerState: DrawerState, + searchHistoryViewModel: SearchHistoryViewModel, ) { val account = getCurrentAccount(accountViewModel) val ctx = LocalContext.current @@ -177,6 +179,8 @@ fun BottomNavActivity( CommunityListActivity( navController = navController, accountViewModel = accountViewModel, + appSettingsViewModel = appSettingsViewModel, + searchHistoryViewModel = searchHistoryViewModel, selectMode = false, siteViewModel = siteViewModel, blurNSFW = appSettings.blurNSFW, diff --git a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt index 40e1eb369..03b236445 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt @@ -92,6 +92,7 @@ fun LookAndFeelActivity( val secureWindowState = rememberBooleanSettingState(settings.secureWindow) val blurNSFW = rememberBooleanSettingState(settings.blurNSFW) val backConfirmationMode = rememberIntSettingState(settings.backConfirmationMode) + val saveSearchHistory = rememberBooleanSettingState(settings.saveSearchHistory) val snackbarHostState = remember { SnackbarHostState() } @@ -118,6 +119,7 @@ fun LookAndFeelActivity( showTextDescriptionsInNavbar = showTextDescriptionsInNavbar.value, blurNSFW = blurNSFW.value, backConfirmationMode = backConfirmationMode.value, + saveSearchHistory = saveSearchHistory.value, ), ) } @@ -317,6 +319,11 @@ fun LookAndFeelActivity( ) }, ) + SettingsCheckbox( + title = { Text(text = stringResource(R.string.save_search_history)) }, + state = saveSearchHistory, + onCheckedChange = { updateAppSettings() }, + ) } }, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 442c0f041..534fea686 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -342,4 +342,8 @@ Show a confirmation dialog on exit Exit Are you sure you want to exit? + Recent searches + Delete %1$s + Communities + Save search history