diff --git a/README.md b/README.md index 52f485620..29f4320dd 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OP | Search Suggestions | ✅ | | Subtitles | ✅ | | Comments | ✅ | -| Search Filters | 🔴 | +| Search Filters | ✅ | ## Contributing diff --git a/app/src/main/java/com/github/libretube/PipedApi.kt b/app/src/main/java/com/github/libretube/PipedApi.kt index 3332c8ca4..5a3c4888f 100644 --- a/app/src/main/java/com/github/libretube/PipedApi.kt +++ b/app/src/main/java/com/github/libretube/PipedApi.kt @@ -25,6 +25,13 @@ interface PipedApi { @Query("filter") filter: String ): SearchResult + @GET("nextpage/search") + suspend fun getSearchResultsNextPage( + @Query("q") searchQuery: String, + @Query("filter") filter: String, + @Query("nextpage") nextPage: String + ): SearchResult + @GET("suggestions") suspend fun getSuggestions(@Query("query") query: String): List diff --git a/app/src/main/java/com/github/libretube/SearchFragment.kt b/app/src/main/java/com/github/libretube/SearchFragment.kt index 6d1865770..e97d7d44d 100644 --- a/app/src/main/java/com/github/libretube/SearchFragment.kt +++ b/app/src/main/java/com/github/libretube/SearchFragment.kt @@ -1,6 +1,7 @@ package com.github.libretube import android.content.Context +import android.content.DialogInterface import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -11,9 +12,9 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager -import android.widget.ArrayAdapter -import android.widget.AutoCompleteTextView +import android.widget.* import android.widget.TextView.* +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager @@ -28,9 +29,15 @@ import kotlinx.coroutines.launch import retrofit2.HttpException import java.io.IOException - class SearchFragment : Fragment() { private val TAG = "SearchFragment" + private var selectedFilter = 0 + private var apiSearchFilter = "all" + private var nextPage : String? = null + private lateinit var searchRecView : RecyclerView + private var searchAdapter : SearchAdapter? = null + private var isLoading : Boolean = true + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { @@ -49,31 +56,71 @@ class SearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val recyclerView = view.findViewById(R.id.search_recycler) + searchRecView = view.findViewById(R.id.search_recycler) val autoTextView = view.findViewById(R.id.autoCompleteTextView) val historyRecycler = view.findViewById(R.id.history_recycler) + val filterImageView = view.findViewById(R.id.filterMenu_imageView) + + var tempSelectedItem = 0 + + filterImageView.setOnClickListener { + val filterOptions = arrayOf( + getString(R.string.all), + getString(R.string.videos), + getString(R.string.channels), + getString(R.string.playlists), + getString(R.string.music_songs), + getString(R.string.music_videos), + getString(R.string.music_albums), + getString(R.string.music_playlists) + ) + + AlertDialog.Builder(view.context) + .setTitle(getString(R.string.choose_filter)) + .setSingleChoiceItems(filterOptions, selectedFilter, DialogInterface.OnClickListener { + _, id -> tempSelectedItem = id + }) + .setPositiveButton(getString(R.string.okay), DialogInterface.OnClickListener { _, _ -> + selectedFilter = tempSelectedItem + apiSearchFilter = when (selectedFilter) { + 0 -> "all" + 1 -> "videos" + 2 -> "channels" + 3 -> "playlists" + 4 -> "music_songs" + 5 -> "music_videos" + 6 -> "music_albums" + 7 -> "music_playlists" + else -> "all" + } + fetchSearch(autoTextView.text.toString()) + }) + .setNegativeButton(getString(R.string.cancel), null) + .create() + .show() + } //show search history - recyclerView.visibility = GONE + searchRecView.visibility = GONE historyRecycler.visibility = VISIBLE historyRecycler.layoutManager = LinearLayoutManager(view.context) var historylist = getHistory() - if (historylist.size != 0) { + if (historylist.isNotEmpty()) { historyRecycler.adapter = SearchHistoryAdapter(requireContext(), historylist, autoTextView) } - recyclerView.layoutManager = GridLayoutManager(view.context, 1) + searchRecView.layoutManager = GridLayoutManager(view.context, 1) autoTextView.requestFocus() val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm!!.showSoftInput(autoTextView, InputMethodManager.SHOW_IMPLICIT) + imm.showSoftInput(autoTextView, InputMethodManager.SHOW_IMPLICIT) autoTextView.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged( s: CharSequence?, @@ -86,27 +133,34 @@ class SearchFragment : Fragment() { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { if (s!! != "") { - recyclerView.visibility = VISIBLE + searchRecView.visibility = VISIBLE historyRecycler.visibility = GONE - recyclerView.adapter = null + searchRecView.adapter = null + + searchRecView.viewTreeObserver + .addOnScrollChangedListener { + if (!searchRecView.canScrollVertically(1)) { + fetchNextSearchItems(autoTextView.text.toString()) + } + + } GlobalScope.launch { fetchSuggestions(s.toString(), autoTextView) - delay(3000) - addtohistory(s.toString()) - fetchSearch(s.toString(), recyclerView) + delay(1000) + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + if (sharedPreferences.getBoolean("search_history_toggle", true)) addtohistory(s.toString()) + fetchSearch(s.toString()) } - - } } override fun afterTextChanged(s: Editable?) { if (s!!.isEmpty()) { - recyclerView.visibility = GONE + searchRecView.visibility = GONE historyRecycler.visibility = VISIBLE var historylist = getHistory() - if (historylist.size != 0) { + if (historylist.isNotEmpty()) { historyRecycler.adapter = SearchHistoryAdapter(requireContext(), historylist, autoTextView) } @@ -116,8 +170,8 @@ class SearchFragment : Fragment() { }) autoTextView.setOnEditorActionListener(OnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_SEARCH) { - hideKeyboard(); - autoTextView.dismissDropDown(); + hideKeyboard() + autoTextView.dismissDropDown() return@OnEditorActionListener true } false @@ -143,10 +197,10 @@ class SearchFragment : Fragment() { autoTextView.setAdapter(adapter) } } - private fun fetchSearch(query: String, recyclerView: RecyclerView){ + private fun fetchSearch(query: String){ lifecycleScope.launchWhenCreated { val response = try { - RetrofitInstance.api.getSearchResults(query, "all") + RetrofitInstance.api.getSearchResults(query, apiSearchFilter) } catch (e: IOException) { println(e) Log.e(TAG, "IOException, you might not have internet connection $e") @@ -155,15 +209,38 @@ class SearchFragment : Fragment() { Log.e(TAG, "HttpException, unexpected response") return@launchWhenCreated } + nextPage = response.nextpage if(response.items!!.isNotEmpty()){ runOnUiThread { - recyclerView.adapter = SearchAdapter(response.items) + searchAdapter = SearchAdapter(response.items) + searchRecView.adapter = searchAdapter } } - + isLoading = false } } + private fun fetchNextSearchItems(query: String){ + lifecycleScope.launchWhenCreated { + if (!isLoading) { + isLoading = true + val response = try { + RetrofitInstance.api.getSearchResultsNextPage(query,apiSearchFilter,nextPage!!) + } catch (e: IOException) { + println(e) + Log.e(TAG, "IOException, you might not have internet connection") + return@launchWhenCreated + } catch (e: HttpException) { + Log.e(TAG, "HttpException, unexpected response," + e.response()) + return@launchWhenCreated + } + nextPage = response.nextpage + searchAdapter?.updateItems(response.items!!) + isLoading = false + } + } + } + private fun Fragment?.runOnUiThread(action: () -> Unit) { this ?: return if (!isAdded) return // Fragment not attached to an Activity diff --git a/app/src/main/java/com/github/libretube/SettingsActivity.kt b/app/src/main/java/com/github/libretube/SettingsActivity.kt index 58384bc17..91fe08dfe 100644 --- a/app/src/main/java/com/github/libretube/SettingsActivity.kt +++ b/app/src/main/java/com/github/libretube/SettingsActivity.kt @@ -9,6 +9,7 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle +import android.system.Os.remove import android.text.TextUtils import android.util.Log import android.view.View @@ -235,6 +236,13 @@ class SettingsActivity : AppCompatActivity(), true } + val clearHistory = findPreference("clear_history") + clearHistory?.setOnPreferenceClickListener { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + sharedPreferences.edit().remove("search_history").commit() + true + } + val about = findPreference("about") about?.setOnPreferenceClickListener { val uri = Uri.parse("https://libre-tube.github.io/") diff --git a/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt index 9f93cecd4..71e431041 100644 --- a/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt +++ b/app/src/main/java/com/github/libretube/adapters/SearchAdapter.kt @@ -18,7 +18,14 @@ import com.github.libretube.obj.SearchItem import com.squareup.picasso.Picasso -class SearchAdapter(private val searchItems: List): RecyclerView.Adapter() { +class SearchAdapter(private val searchItems: MutableList): RecyclerView.Adapter() { + + fun updateItems(newItems: List){ + var searchItemsSize = searchItems.size + searchItems.addAll(newItems) + notifyItemRangeInserted(searchItemsSize, newItems.size) + } + override fun getItemCount(): Int { return searchItems.size } @@ -52,19 +59,17 @@ class CustomViewHolder1(private val v: View): RecyclerView.ViewHolder(v){ private fun bindWatch(item: SearchItem) { val thumbnailImage = v.findViewById(R.id.search_thumbnail) - Picasso.get().load(item.thumbnail).into(thumbnailImage) + Picasso.get().load(item.thumbnail).fit().centerCrop().into(thumbnailImage) val thumbnailDuration = v.findViewById(R.id.search_thumbnail_duration) thumbnailDuration.text = DateUtils.formatElapsedTime(item.duration!!) val channelImage = v.findViewById(R.id.search_channel_image) - Picasso.get().load(item.uploaderAvatar).into(channelImage) + Picasso.get().load(item.uploaderAvatar).fit().centerCrop().into(channelImage) val title = v.findViewById(R.id.search_description) - if (item.title!!.length > 60) { - title.text = item.title?.substring(0, 60) + "..." - } else { - title.text = item.title - } + title.text = if (item.title!!.length > 60) item.title?.substring(0, 60) + "..." else item.title val views = v.findViewById(R.id.search_views) - views.text = item.views.formatShort() +" • "+item.uploadedDate + val viewsString = if (item.views?.toInt() != -1) item.views.formatShort() else "" + val uploadDate = if (item.uploadedDate != null) item.uploadedDate else "" + views.text = if (viewsString != "" && uploadDate != "") viewsString + " • " + uploadDate else viewsString + uploadDate val channelName = v.findViewById(R.id.search_channel_name) channelName.text = item.uploaderName v.setOnClickListener{ @@ -88,7 +93,7 @@ class CustomViewHolder1(private val v: View): RecyclerView.ViewHolder(v){ } private fun bindChannel(item: SearchItem) { val channelImage = v.findViewById(R.id.search_channel_image) - Picasso.get().load(item.thumbnail).into(channelImage) + Picasso.get().load(item.thumbnail).fit().centerCrop().into(channelImage) val channelName = v.findViewById(R.id.search_channel_name) channelName.text = item.name val channelViews = v.findViewById(R.id.search_views) @@ -102,15 +107,15 @@ class CustomViewHolder1(private val v: View): RecyclerView.ViewHolder(v){ } private fun bindPlaylist(item: SearchItem) { val playlistImage = v.findViewById(R.id.search_thumbnail) - Picasso.get().load(item.thumbnail).into(playlistImage) + Picasso.get().load(item.thumbnail).fit().centerCrop().into(playlistImage) val playlistNumber = v.findViewById(R.id.search_playlist_number) - playlistNumber.text = item.videos.toString() + if (item.videos?.toInt() != -1) playlistNumber.text = item.videos.toString() val playlistName = v.findViewById(R.id.search_description) playlistName.text = item.name val playlistChannelName = v.findViewById(R.id.search_name) playlistChannelName.text = item.uploaderName val playlistVideosNumber = v.findViewById(R.id.search_playlist_videos) - playlistVideosNumber.text = item.videos.toString()+" videos" + if (item.videos?.toInt() != -1) playlistVideosNumber.text = v.context.getString(R.string.videoCount, item.videos.toString()) v.setOnClickListener { //playlist clicked val activity = v.context as MainActivity @@ -128,4 +133,4 @@ class CustomViewHolder1(private val v: View): RecyclerView.ViewHolder(v){ } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/obj/SearchResult.kt b/app/src/main/java/com/github/libretube/obj/SearchResult.kt index 297619799..13172c9a9 100644 --- a/app/src/main/java/com/github/libretube/obj/SearchResult.kt +++ b/app/src/main/java/com/github/libretube/obj/SearchResult.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties @JsonIgnoreProperties(ignoreUnknown = true) data class SearchResult( - val items: List? = listOf(), + val items: MutableList? = arrayListOf(), val nextpage: String? ="", val suggestion: String?="", val corrected: Boolean? = null diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 000000000..e0b21ad93 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_trash.xml b/app/src/main/res/drawable/ic_trash.xml new file mode 100644 index 000000000..d52cfe83e --- /dev/null +++ b/app/src/main/res/drawable/ic_trash.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 9a2b0e083..24d8b703c 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -6,41 +6,63 @@ android:layout_height="match_parent" tools:context=".SearchFragment"> - - + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="16dp" + app:cardCornerRadius="27dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + app:hintEnabled="false"> - + - + + + + + @@ -297,21 +319,18 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/outlinedTextField" + app:layout_constraintTop_toBottomOf="@+id/searchbar_holder" android:visibility="gone"/> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_empty.xml b/app/src/main/res/layout/layout_empty.xml new file mode 100644 index 000000000..006fd4963 --- /dev/null +++ b/app/src/main/res/layout/layout_empty.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/playlist_search_row.xml b/app/src/main/res/layout/playlist_search_row.xml index a4d451b23..2b3eb1086 100644 --- a/app/src/main/res/layout/playlist_search_row.xml +++ b/app/src/main/res/layout/playlist_search_row.xml @@ -43,21 +43,17 @@ android:id="@+id/search_playlist_number" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="10" - android:textColor="#ECE4E4" + android:layout_centerInParent="true" android:layout_centerVertical="true" - android:layout_centerHorizontal="true" - /> + android:textColor="#ECE4E4" /> + android:layout_centerInParent="true" + app:srcCompat="@drawable/ic_playlist" /> @@ -67,7 +63,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/card_playlist" app:layout_constraintTop_toTopOf="parent" /> @@ -77,7 +72,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/card_playlist" app:layout_constraintTop_toBottomOf="@+id/search_description" /> @@ -86,7 +80,6 @@ android:id="@+id/search_playlist_videos" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="TextView" android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/guideline" diff --git a/app/src/main/res/layout/video_channel_row.xml b/app/src/main/res/layout/video_channel_row.xml index 6835d1997..ad74d023b 100644 --- a/app/src/main/res/layout/video_channel_row.xml +++ b/app/src/main/res/layout/video_channel_row.xml @@ -50,7 +50,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/card_search_thumbnail" app:layout_constraintTop_toTopOf="parent" /> @@ -60,7 +59,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/card_search_thumbnail" app:layout_constraintTop_toBottomOf="@+id/channel_description" /> diff --git a/app/src/main/res/layout/video_search_row.xml b/app/src/main/res/layout/video_search_row.xml index 444473b2e..5d26d04b3 100644 --- a/app/src/main/res/layout/video_search_row.xml +++ b/app/src/main/res/layout/video_search_row.xml @@ -53,7 +53,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/card_search_thumbnail" app:layout_constraintTop_toTopOf="parent" /> @@ -63,7 +62,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/card_search_thumbnail" app:layout_constraintTop_toBottomOf="@+id/search_description" /> @@ -83,7 +81,6 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="12dp" - android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/search_channel_image" app:layout_constraintTop_toBottomOf="@+id/search_views" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc68be368..104aada90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,6 +72,18 @@ No Internet Connection Retry Comments + Choose search filter + Channels + All + Playlists + Ok + History + Search History + Clear History + Music Songs + Music Videos + Music Albums + Music Playlists Default Tab SponsorBlock Uses API from https://sponsor.ajay.app/ diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 18349aaa1..d28f3a464 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -109,6 +109,21 @@ + + + + + + + +