diff --git a/app/build.gradle b/app/build.gradle index 6e3b3e96..6538269f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlin-android-extensions' } android { @@ -59,4 +60,7 @@ dependencies { implementation("com.google.android.material:material:1.3.0") implementation "androidx.preference:preference-ktx:1.1.1" + + implementation 'com.github.bumptech.glide:glide:4.12.0' + implementation 'jp.wasabeef:glide-transformations:4.0.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 36e9fb0d..6c6c9f43 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + () + + override fun search(query: String): ArrayList? { + val list = apis.filter { a -> + a.name != this.name && (providersActive.size == 0 || providersActive.contains(a.name)) + }.pmap { a -> + a.search(query) + } + + var maxCount = 0 + var providerCount = 0 + for (res in list) { + if (res != null) { + if (res.size > maxCount) { + maxCount = res.size + } + providerCount++ + } + } + + if (providerCount == 0) return null + if (maxCount == 0) return ArrayList() + + val result = ArrayList() + for (i in 0..maxCount) { + for (res in list) { + if (res != null) { + if (i < res.size) { + result.add(res[i]) + } + } + } + } + + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 86a9be05..b09d5d3c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1,5 +1,7 @@ package com.lagradost.cloudstream3 +import android.app.Activity +import androidx.preference.PreferenceManager import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule @@ -13,9 +15,20 @@ val mapper = JsonMapper.builder().addModule(KotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! object APIHolder { + val allApi = AllProvider() + + private const val defProvider = 0 + val apis = arrayListOf( ShiroProvider() ) + + fun Activity.getApiSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + + return settingsManager.getStringSet(this.getString(R.string.search_providers_list_key), + setOf(apis[defProvider].name))?.toHashSet() ?: hashSetOf(apis[defProvider].name) + } } @@ -35,6 +48,17 @@ abstract class MainAPI { } } +fun MainAPI.fixUrl(url: String): String { + if (url.startsWith('/')) { + return mainUrl + url + } + else if(!url.startsWith("http") && !url.startsWith("//")) { + return "$mainUrl/$url" + } + return url +} + + data class Link( val name: String, val url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt new file mode 100644 index 00000000..f5266777 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3 + +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList + +//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections +fun Iterable.pmap( + numThreads: Int = maxOf(Runtime.getRuntime().availableProcessors() - 2, 1), + exec: ExecutorService = Executors.newFixedThreadPool(numThreads), + transform: (T) -> R, +): List { + + // default size is just an inlined version of kotlin.collections.collectionSizeOrDefault + val defaultSize = if (this is Collection<*>) this.size else 10 + val destination = Collections.synchronizedList(ArrayList(defaultSize)) + + for (item in this) { + exec.submit { destination.add(transform(item)) } + } + + exec.shutdown() + exec.awaitTermination(1, TimeUnit.DAYS) + + return ArrayList(destination) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt index 87586a08..32e0dc96 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt @@ -1,20 +1,52 @@ package com.lagradost.cloudstream3 import android.app.Activity +import android.content.res.Resources import android.view.View +import androidx.preference.PreferenceManager object UIHelper { - fun Activity.getStatusBarHeight(): Int { + val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() + val Float.toPx: Float get() = (this * Resources.getSystem().displayMetrics.density) + val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() + val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) + + fun Activity.loadResult(url: String, apiName: String) { + /*this.runOnUiThread { + this.supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.enter_anim, R.anim.exit_anim, R.anim.pop_enter, R.anim.pop_exit) + .add(R.id.homeRoot, ResultFragment().newInstance(url, apiName)) + .commit() + }*/ + } + + private fun Activity.getStatusBarHeight(): Int { var result = 0 val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") if (resourceId > 0) { result = resources.getDimensionPixelSize(resourceId) } return result - } fun Activity.fixPaddingStatusbar(v: View) { v.setPadding(v.paddingLeft, v.paddingTop + getStatusBarHeight(), v.paddingRight, v.paddingBottom) } + + private fun Activity.getGridFormat(): String { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager.getString(getString(R.string.grid_format_key), "grid")!! + } + + fun Activity.getGridFormatId(): Int { + return when (getGridFormat()) { + "list" -> R.layout.search_result_compact + "compact_list" -> R.layout.search_result_super_compact + else -> R.layout.search_result_grid + } + } + + fun Activity.getGridIsCompact(): Boolean { + return getGridFormat() != "grid" + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt index 361ebd09..687fa854 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt @@ -3,9 +3,9 @@ package com.lagradost.cloudstream3.animeproviders import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.* -import org.jsoup.Jsoup -import java.lang.Exception import java.net.URLEncoder +import java.util.* + class ShiroProvider : MainAPI() { companion object { @@ -42,6 +42,12 @@ class ShiroProvider : MainAPI() { @JsonProperty("_id") val _id: String, @JsonProperty("slug") val slug: String, @JsonProperty("name") val name: String, + @JsonProperty("episodeCount") val episodeCount: String, + @JsonProperty("language") val language: String, + @JsonProperty("type") val type: String, + @JsonProperty("year") val year: String, + @JsonProperty("canonicalTitle") val canonicalTitle: String, + @JsonProperty("english") val english: String?, ) data class ShiroSearchResponse( @@ -67,16 +73,50 @@ class ShiroProvider : MainAPI() { ) override fun search(query: String): ArrayList? { - if (!autoLoadToken()) return null - val returnValue: ArrayList = ArrayList() - val response = khttp.get("https://tapi.shiro.is/advanced?search=${ - URLEncoder.encode( - query, - "UTF-8" - ) - }&token=$token") - val mapped = response.let { mapper.readValue(it.text) } + try { + if (!autoLoadToken()) return null + val returnValue: ArrayList = ArrayList() + val response = khttp.get("https://tapi.shiro.is/advanced?search=${ + URLEncoder.encode( + query, + "UTF-8" + ) + }&token=$token".replace("+", "%20")) + println(response.text) + val mapped = response.let { mapper.readValue(it.text) } + for (i in mapped.data.nav.currentPage.items) { - return returnValue + val type = when (i.type) { + "TV" -> TvType.Anime + "OVA" -> TvType.ONA + "movie" -> TvType.Movie + else -> TvType.Anime + } + val isDubbed = i.language == "dubbed" + val set: EnumSet = EnumSet.noneOf(DubStatus::class.java) + + if (isDubbed) + set.add(DubStatus.HasDub) + else + set.add(DubStatus.HasSub) + val episodeCount = i.episodeCount.toInt() + + returnValue.add(AnimeSearchResponse( + i.english ?: i.canonicalTitle, + "$mainUrl/${i.slug}", + this.name, + type, + "https://cdn.shiro.is/${i.image}", + i.year.toInt(), + i.canonicalTitle, + set, + if (isDubbed) episodeCount else null, + if (!isDubbed) episodeCount else null, + )) + } + return returnValue + } catch (e: Exception) { + return null + } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt new file mode 100644 index 00000000..657fe6dc --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -0,0 +1,124 @@ +package com.lagradost.cloudstream3.ui.search + +import android.app.Activity +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.model.GlideUrl +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.UIHelper.getGridFormatId +import com.lagradost.cloudstream3.UIHelper.getGridIsCompact +import com.lagradost.cloudstream3.UIHelper.loadResult +import com.lagradost.cloudstream3.UIHelper.toPx +import com.lagradost.cloudstream3.ui.AutofitRecyclerView +import kotlinx.android.synthetic.main.search_result_compact.view.* +import kotlinx.android.synthetic.main.search_result_compact.view.backgroundCard +import kotlinx.android.synthetic.main.search_result_compact.view.imageText +import kotlinx.android.synthetic.main.search_result_compact.view.imageView +import kotlinx.android.synthetic.main.search_result_grid.view.* +import kotlin.math.roundToInt + +class SearchAdapter( + activity: Activity, + animeList: ArrayList, + resView: AutofitRecyclerView, +) : + RecyclerView.Adapter() { + var cardList = animeList + private var activity: Activity = activity + var resView: AutofitRecyclerView? = resView + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val layout = activity.getGridFormatId() + return CardViewHolder( + LayoutInflater.from(parent.context).inflate(layout, parent, false), + activity, + resView!! + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is CardViewHolder -> { + holder.bind(cardList[position]) + } + + } + } + + override fun getItemCount(): Int { + return cardList.size + } + + class CardViewHolder + constructor(itemView: View, _activity: Activity, resView: AutofitRecyclerView) : RecyclerView.ViewHolder(itemView) { + val activity = _activity + val cardView: ImageView = itemView.imageView + val cardText: TextView = itemView.imageText + val text_type: TextView? = itemView.text_type + val text_is_dub: TextView? = itemView.text_is_dub + val text_is_sub: TextView? = itemView.text_is_sub + + //val cardTextExtra: TextView? = itemView.imageTextExtra + //val imageTextProvider: TextView? = itemView.imageTextProvider + val bg = itemView.backgroundCard + val compactView = activity.getGridIsCompact() + private val coverHeight: Int = if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt() + + fun bind(card: Any) { + if (card is SearchResponse) { // GENERIC + if (!compactView) { + cardView.apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + coverHeight + ) + } + } + + text_type?.text = when (card.type) { + TvType.Anime -> "Anime" + TvType.Movie -> "Movie" + TvType.ONA -> "ONA" + TvType.TvSeries -> "TV" + } + + text_is_dub?.visibility = View.GONE + text_is_sub?.visibility = View.GONE + + cardText.text = card.name + + //imageTextProvider.text = card.apiName + + val glideUrl = + GlideUrl(card.posterUrl) + activity.let { + Glide.with(it) + .load(glideUrl) + .into(cardView) + } + + bg.setOnClickListener { + activity.loadResult(card.url, card.apiName) + } + + when (card) { + is AnimeSearchResponse -> { + if (card.dubStatus?.contains(DubStatus.HasDub) == true) { + text_is_dub?.visibility = View.VISIBLE + } + if (card.dubStatus?.contains(DubStatus.HasSub) == true) { + text_is_sub?.visibility = View.VISIBLE + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 4bbfc7ee..136f26c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -1,15 +1,29 @@ package com.lagradost.cloudstream3.ui.search +import android.content.DialogInterface +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.allApi +import com.lagradost.cloudstream3.APIHolder.apis +import com.lagradost.cloudstream3.APIHolder.getApiSettings import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.UIHelper.getGridIsCompact +import kotlinx.android.synthetic.main.fragment_search.* +import kotlin.concurrent.thread class SearchFragment : Fragment() { @@ -18,7 +32,7 @@ class SearchFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View? { searchViewModel = ViewModelProvider(this).get(SearchViewModel::class.java) @@ -29,6 +43,91 @@ class SearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // activity?.fixPaddingStatusbar(searchRoot) + activity?.fixPaddingStatusbar(searchRoot) + + val compactView = activity?.getGridIsCompact() ?: false + val spanCountLandscape = if (compactView) 2 else 6 + val spanCountPortrait = if (compactView) 1 else 3 + val orientation = resources.configuration.orientation + + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + cardSpace.spanCount = spanCountLandscape + } else { + cardSpace.spanCount = spanCountPortrait + } + + val adapter: RecyclerView.Adapter? = activity?.let { + SearchAdapter( + it, + ArrayList(), + cardSpace, + ) + } + + cardSpace.adapter = adapter + search_loading_bar.alpha = 0f + + val search_exit_icon = main_search.findViewById(androidx.appcompat.R.id.search_close_btn) + val search_mag_icon = main_search.findViewById(androidx.appcompat.R.id.search_mag_icon) + search_mag_icon.scaleX = 0.65f + search_mag_icon.scaleY = 0.65f + search_filter.setOnClickListener { + val apiNamesSetting = activity?.getApiSettings() + if (apiNamesSetting != null) { + val apiNames = apis.map { it.name } + val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext()) + + builder.setMultiChoiceItems(apiNames.toTypedArray(), + apiNames.map { a -> apiNamesSetting.contains(a) }.toBooleanArray() + ) { _, position: Int, checked: Boolean -> + val apiNamesSettingLocal = activity?.getApiSettings() + if (apiNamesSettingLocal != null) { + val settingsManagerLocal = PreferenceManager.getDefaultSharedPreferences(activity) + if (checked) { + apiNamesSettingLocal.add(apiNames[position]) + } else { + apiNamesSettingLocal.remove(apiNames[position]) + } + + val edit = settingsManagerLocal.edit() + edit.putStringSet(getString(R.string.search_providers_list_key), + apiNames.filter { a -> apiNamesSettingLocal.contains(a) }.toSet()) + edit.apply() + allApi.providersActive = apiNamesSettingLocal + } + } + builder.setTitle("Search Providers") + builder.setNegativeButton("Cancel") { _, _ -> } + builder.show() + } + } + + main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + search_exit_icon.alpha = 0f + search_loading_bar.alpha = 1f + thread { + val data = allApi.search(query)//MainActivity.activeAPI.search(query) + activity?.runOnUiThread { + if (data == null) { + Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() + } else { + (cardSpace.adapter as SearchAdapter).cardList = data + (cardSpace.adapter as SearchAdapter).notifyDataSetChanged() + } + search_exit_icon.alpha = 1f + search_loading_bar.alpha = 0f + } + } + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + return true + } + }) + + main_search.onActionViewExpanded() + } } \ No newline at end of file diff --git a/app/src/main/res/anim/enter_anim.xml b/app/src/main/res/anim/enter_anim.xml new file mode 100644 index 00000000..718b764f --- /dev/null +++ b/app/src/main/res/anim/enter_anim.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/exit_anim.xml b/app/src/main/res/anim/exit_anim.xml new file mode 100644 index 00000000..ee810757 --- /dev/null +++ b/app/src/main/res/anim/exit_anim.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/nav_enter_anim.xml b/app/src/main/res/anim/nav_enter_anim.xml new file mode 100644 index 00000000..84fa9e97 --- /dev/null +++ b/app/src/main/res/anim/nav_enter_anim.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/nav_exit_anim.xml b/app/src/main/res/anim/nav_exit_anim.xml new file mode 100644 index 00000000..97065514 --- /dev/null +++ b/app/src/main/res/anim/nav_exit_anim.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/nav_pop_enter.xml b/app/src/main/res/anim/nav_pop_enter.xml new file mode 100644 index 00000000..84fa9e97 --- /dev/null +++ b/app/src/main/res/anim/nav_pop_enter.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/nav_pop_exit.xml b/app/src/main/res/anim/nav_pop_exit.xml new file mode 100644 index 00000000..97065514 --- /dev/null +++ b/app/src/main/res/anim/nav_pop_exit.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/pop_enter.xml b/app/src/main/res/anim/pop_enter.xml new file mode 100644 index 00000000..718b764f --- /dev/null +++ b/app/src/main/res/anim/pop_enter.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/pop_exit.xml b/app/src/main/res/anim/pop_exit.xml new file mode 100644 index 00000000..ee810757 --- /dev/null +++ b/app/src/main/res/anim/pop_exit.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dub_bg_color.xml b/app/src/main/res/drawable/dub_bg_color.xml new file mode 100644 index 00000000..bd68e0f6 --- /dev/null +++ b/app/src/main/res/drawable/dub_bg_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/sub_bg_color.xml b/app/src/main/res/drawable/sub_bg_color.xml new file mode 100644 index 00000000..90113428 --- /dev/null +++ b/app/src/main/res/drawable/sub_bg_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/title_shadow.xml b/app/src/main/res/drawable/title_shadow.xml new file mode 100644 index 00000000..bb05dfbc --- /dev/null +++ b/app/src/main/res/drawable/title_shadow.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/type_bg_color.xml b/app/src/main/res/drawable/type_bg_color.xml new file mode 100644 index 00000000..c6ca4367 --- /dev/null +++ b/app/src/main/res/drawable/type_bg_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_result_compact.xml b/app/src/main/res/layout/search_result_compact.xml new file mode 100644 index 00000000..ed73bb01 --- /dev/null +++ b/app/src/main/res/layout/search_result_compact.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_result_grid.xml b/app/src/main/res/layout/search_result_grid.xml new file mode 100644 index 00000000..fd7dff71 --- /dev/null +++ b/app/src/main/res/layout/search_result_grid.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_result_super_compact.xml b/app/src/main/res/layout/search_result_super_compact.xml new file mode 100644 index 00000000..d5ba157a --- /dev/null +++ b/app/src/main/res/layout/search_result_super_compact.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ea966be2..1bb88936 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -21,4 +21,10 @@ #00000000 #FFF + #3b65f5 + #4D3B65F5 + #F53B66 + #4DF53B66 + #F54A3B + #4DF54A3B \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c156143..f1c90c46 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,9 @@ Downloads Search... Change Providers + search_providers_list + grid_format + Poster + No Data + Shadow \ No newline at end of file