Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import com.github.andreyasadchy.xtra.model.ui.LocalFollowChannel
import com.github.andreyasadchy.xtra.model.ui.LocalFollowGame
import com.github.andreyasadchy.xtra.model.ui.OfflineVideo
import com.github.andreyasadchy.xtra.model.ui.SavedFilter
import com.github.andreyasadchy.xtra.model.ui.RecentSearch
import com.github.andreyasadchy.xtra.model.ui.SortChannel
import com.github.andreyasadchy.xtra.model.ui.SortGame
import com.github.andreyasadchy.xtra.model.ui.TranslateAllMessagesUser
import com.github.andreyasadchy.xtra.model.ui.VodBookmarkIgnoredUser

@Database(
entities = [OfflineVideo::class, RecentEmote::class, VideoPosition::class, LocalFollowChannel::class, LocalFollowGame::class, Bookmark::class, VodBookmarkIgnoredUser::class, SortChannel::class, SortGame::class, ShownNotification::class, NotificationUser::class, TranslateAllMessagesUser::class, SavedFilter::class],
version = 32
entities = [OfflineVideo::class, RecentEmote::class, VideoPosition::class, LocalFollowChannel::class, LocalFollowGame::class, Bookmark::class, VodBookmarkIgnoredUser::class, SortChannel::class, SortGame::class, ShownNotification::class, NotificationUser::class, TranslateAllMessagesUser::class, SavedFilter::class, RecentSearch::class],
version = 33
)
abstract class AppDatabase : RoomDatabase() {

Expand All @@ -35,4 +36,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun notificationsDao(): NotificationUsersDao
abstract fun translateAllMessagesUsersDao(): TranslateAllMessagesUsersDao
abstract fun savedFiltersDao(): SavedFiltersDao
abstract fun recentSearchDao(): RecentSearchDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.github.andreyasadchy.xtra.db

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.andreyasadchy.xtra.model.ui.RecentSearch

@Dao
interface RecentSearchDao {

@Query("SELECT * FROM recent_search ORDER BY lastSearched DESC LIMIT :limit")
fun recentSearches(limit: Int): List<RecentSearch>

@Query("SELECT * FROM recent_search WHERE `query` = :query")
fun find(query: String) : RecentSearch?

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recentSearch: RecentSearch)

@Delete
fun delete(recentSearch: RecentSearch)

@Query("DELETE FROM recent_search")
fun deleteAll()
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.github.andreyasadchy.xtra.db.LocalFollowsGameDao
import com.github.andreyasadchy.xtra.db.NotificationUsersDao
import com.github.andreyasadchy.xtra.db.RecentEmotesDao
import com.github.andreyasadchy.xtra.db.SavedFiltersDao
import com.github.andreyasadchy.xtra.db.RecentSearchDao
import com.github.andreyasadchy.xtra.db.ShownNotificationsDao
import com.github.andreyasadchy.xtra.db.SortChannelDao
import com.github.andreyasadchy.xtra.db.SortGameDao
Expand All @@ -24,6 +25,7 @@ import com.github.andreyasadchy.xtra.repository.LocalFollowGameRepository
import com.github.andreyasadchy.xtra.repository.NotificationUsersRepository
import com.github.andreyasadchy.xtra.repository.OfflineRepository
import com.github.andreyasadchy.xtra.repository.SavedFiltersRepository
import com.github.andreyasadchy.xtra.repository.RecentSearchRepository
import com.github.andreyasadchy.xtra.repository.ShownNotificationsRepository
import com.github.andreyasadchy.xtra.repository.SortChannelRepository
import com.github.andreyasadchy.xtra.repository.SortGameRepository
Expand Down Expand Up @@ -83,6 +85,10 @@ class DatabaseModule {
@Provides
fun providesSavedFiltersRepository(savedFiltersDao: SavedFiltersDao): SavedFiltersRepository = SavedFiltersRepository(savedFiltersDao)

@Singleton
@Provides
fun providesRecentSearchRepository(recentSearchDao: RecentSearchDao): RecentSearchRepository = RecentSearchRepository(recentSearchDao)

@Singleton
@Provides
fun providesVideosDao(database: AppDatabase): VideosDao = database.videos()
Expand Down Expand Up @@ -135,6 +141,10 @@ class DatabaseModule {
@Provides
fun providesSavedFiltersDao(database: AppDatabase): SavedFiltersDao = database.savedFiltersDao()

@Singleton
@Provides
fun providesRecentSearchDao(database: AppDatabase): RecentSearchDao = database.recentSearchDao()

@Singleton
@Provides
fun providesAppDatabase(application: Application): AppDatabase =
Expand Down Expand Up @@ -326,6 +336,11 @@ class DatabaseModule {
db.execSQL("CREATE TABLE IF NOT EXISTS filters (id INTEGER NOT NULL, gameId TEXT, gameSlug TEXT, gameName TEXT, tags TEXT, languages TEXT, PRIMARY KEY (id))")
}
},
object : Migration(32, 33) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS recent_search (id INTEGER NOT NULL, query TEXT NOT NULL, lastSearched INTEGER NOT NULL, PRIMARY KEY (id))")
}
},
)
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.andreyasadchy.xtra.model.ui

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "recent_search")
class RecentSearch(
val query: String,
var lastSearched: Long,
) {
@PrimaryKey(autoGenerate = true)
var id = 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.github.andreyasadchy.xtra.repository

import android.util.Log
import com.github.andreyasadchy.xtra.db.RecentSearchDao
import com.github.andreyasadchy.xtra.model.ui.RecentSearch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class RecentSearchRepository @Inject constructor(
private val recentSearchDao: RecentSearchDao
) {
suspend fun loadRecentSearches(limit: Int): List<RecentSearch> = withContext(Dispatchers.IO) {
recentSearchDao.recentSearches(limit)
}

suspend fun find(query: String) = withContext(Dispatchers.IO) {
recentSearchDao.find(query)
}

suspend fun save(recentSearch: RecentSearch) = withContext(Dispatchers.IO) {
recentSearchDao.insert(recentSearch)
}

suspend fun delete(recentSearch: RecentSearch) = withContext(Dispatchers.IO) {
recentSearchDao.delete(recentSearch)
}

suspend fun deleteAll() = withContext(Dispatchers.IO) {
recentSearchDao.deleteAll()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.github.andreyasadchy.xtra.ui.search

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.github.andreyasadchy.xtra.R
import com.github.andreyasadchy.xtra.model.ui.RecentSearch

class RecentSearchAdapter(
private val onRecentSearchItemSelected: (String) -> Unit,
private val onRecentSearchItemInserted: (String) -> Unit,
private val onRecentSearchItemLongClick: (RecentSearch) -> Unit,
) : ListAdapter<RecentSearch, RecentSearchAdapter.ViewHolder>(
object : DiffUtil.ItemCallback<RecentSearch>() {
override fun areItemsTheSame(oldItem: RecentSearch, newItem: RecentSearch): Boolean = oldItem.query == newItem.query
override fun areContentsTheSame(oldItem: RecentSearch, newItem: RecentSearch): Boolean = oldItem.query == newItem.query
}) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fragment_recent_search_list_item, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}

inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView.findViewById(R.id.recentSearchText)
private val recentSearchIcon : LinearLayout = itemView.findViewById(R.id.recentSearchSelect)
private val recentSearchInsert : LinearLayout = itemView.findViewById(R.id.recentSearchInsert)

fun bind(item: RecentSearch) {
textView.text = item.query
recentSearchIcon.setOnClickListener {
onRecentSearchItemSelected(item.query)
}
recentSearchIcon.setOnLongClickListener {
onRecentSearchItemLongClick(item)
true
}
recentSearchInsert.setOnClickListener {
onRecentSearchItemInserted(item.query)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.github.andreyasadchy.xtra.ui.search

import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.andreyasadchy.xtra.R
import com.github.andreyasadchy.xtra.databinding.FragmentRecentSearchBinding
import com.github.andreyasadchy.xtra.model.ui.RecentSearch
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

@AndroidEntryPoint
class RecentSearchesFragment : Fragment() {
private var _binding: FragmentRecentSearchBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModels<SearchPagerViewModel>(ownerProducer = { requireParentFragment() })
private lateinit var onItemSelected: (String) -> Unit
private lateinit var onItemInserted: (String) -> Unit
private lateinit var onItemLongClicked: (RecentSearch) -> Unit

override fun onAttach(context: Context) {
super.onAttach(context)
onItemSelected = (requireParentFragment() as SearchPagerFragment)::setSelectedQuery
onItemInserted = (requireParentFragment() as SearchPagerFragment)::setInsertedQuery
onItemLongClicked = { item ->
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.delete_recent_search_item)).setTitle(item.query)
.setPositiveButton(getString(R.string.delete))
{ _: DialogInterface, _: Int ->
viewModel.deleteRecentSearch(item)
}
.setNegativeButton(getString(android.R.string.cancel), null)
.setCancelable(false)
.create().show()
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val recentSearchAdapter = RecentSearchAdapter(
onItemSelected,
onItemInserted,
onItemLongClicked
)

viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.recentSearches.collectLatest {
recentSearchAdapter.submitList(it)
}
}
}

binding.recentSearchesList.adapter = recentSearchAdapter
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentRecentSearchBinding.inflate(inflater, container, false)
return binding.root
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ class SearchPagerFragment : BaseNetworkFragment(), FragmentHost {
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
private val viewModel: SearchPagerViewModel by viewModels()
private var recentSearchesFragment: RecentSearchesFragment? = null
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it would be nicer to have a recyclerview adapter on each search fragment that contains the recent search items so that you could just swap the adapter to show recent searches instead of having to deal with this fragment here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on each search fragment

I don't understand what you mean there. Are you referring to StreamSearchFragment, VideoSearchFragment, etc.?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the RecentSearches would only need to be related to the SearchPager. I don’t see what connection it would have with the fragments of the results.
I think we are thinking about different implementations.

Screen_recording_20251107_064804.mp4

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the recyclerview could show the recent searches instead of the search results when the search text box is empty

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand what you're saying: it's about placing the recent search items in the same RecyclerView and not creating a new fragment, right?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

private var firstLaunch = true
private var suppressQueryChange = false

override val currentFragment: Fragment?
get() = childFragmentManager.findFragmentByTag("f${binding.viewPager.currentItem}")

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
firstLaunch = savedInstanceState == null
setSearchHistoryFragment()
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Expand Down Expand Up @@ -234,22 +237,53 @@ class SearchPagerFragment : BaseNetworkFragment(), FragmentHost {
}

override fun onQueryTextChange(newText: String): Boolean {
if (suppressQueryChange)
return false
job?.cancel()
if (newText.isNotEmpty()) {
binding.viewPager.visibility = View.VISIBLE
binding.tabLayout.visibility = View.VISIBLE
binding.recentSearchesContainer.visibility = View.GONE
job = lifecycleScope.launch {
delay(750)
withResumed {
(currentFragment as? Searchable)?.search(newText)
if (requireContext().prefs().getBoolean(C.STORE_RECENT_SEARCHES, true)) {
viewModel.saveRecentSearch(newText)
}
}
}
} else {
(currentFragment as? Searchable)?.search(newText) //might be null on rotation, so as?

binding.viewPager.visibility = View.GONE
binding.tabLayout.visibility = View.GONE
setSearchHistoryFragment()
binding.recentSearchesContainer.visibility = View.VISIBLE
}
return false
}
})
}

fun setSelectedQuery(query: String) {
// In case the selected item has the same query as the searchView.
binding.searchView.setQuery(null, false)

binding.searchView.setQuery(query, true)
}

fun setInsertedQuery(query: String) {
suppressQueryChange = true
binding.searchView.setQuery(query, false)
suppressQueryChange = false
}

private fun setSearchHistoryFragment() {
recentSearchesFragment = RecentSearchesFragment()
childFragmentManager.beginTransaction().replace(R.id.recentSearchesContainer, recentSearchesFragment!!).commit()
}

private var userResult: Pair<Int?, String?>? = null

private fun viewUserResult() {
Expand Down
Loading