diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fdbec1ead..61a9130cd 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,8 +1,6 @@ - - - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bfef59df..d5d35ec44 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/build.gradle b/app/build.gradle index be4098181..73178ec24 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,8 +60,8 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6' - implementation 'androidx.core:core-ktx:1.3.0-rc01' - implementation 'androidx.appcompat:appcompat:1.2.0-rc01' + implementation 'androidx.core:core-ktx:1.4.0-alpha01' + implementation 'androidx.appcompat:appcompat:1.3.0-alpha01' implementation 'androidx.activity:activity-ktx:1.2.0-alpha05' implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha05' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha03' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7d5df3091..b4dcb53c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -69,6 +69,8 @@ + > = flow { + val sources = MangaProviderFactory.getSources(false) + for (source in sources) { + val provider = MangaProviderFactory.create(source) + val list = provider.getList(0, query, SortOrder.POPULARITY) + emit(list.take(4)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarAdapter.kt new file mode 100644 index 000000000..8e6ca78f0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarAdapter.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.ui.common.list + +import android.view.ViewGroup + +class ProgressBarAdapter : BaseRecyclerAdapter() { + + var isVisible: Boolean + get() = dataSet.isNotEmpty() + set(value) { + if (value == dataSet.isEmpty()) { + if (value) { + appendItem(true) + } else { + removeItemAt(0) + } + } + } + + override fun getExtra(item: Boolean, position: Int) = Unit + + override fun onCreateViewHolder(parent: ViewGroup) = ProgressBarHolder(parent) + + override fun onGetItemId(item: Boolean) = 1L +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarHolder.kt new file mode 100644 index 000000000..0b21be9a7 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarHolder.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.ui.common.list + +import android.view.View +import android.view.ViewGroup +import kotlinx.android.synthetic.main.item_progress.* +import org.koitharu.kotatsu.R + +class ProgressBarHolder(parent: ViewGroup) : + BaseViewHolder(parent, R.layout.item_progress) { + + override fun onBind(data: Boolean, extra: Unit) { + progressBar.visibility = if (data) { + View.VISIBLE + } else { + View.INVISIBLE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt index b2aabcf64..1c1c62590 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt @@ -32,6 +32,7 @@ import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment import org.koitharu.kotatsu.ui.list.tracklogs.FeedFragment import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderState +import org.koitharu.kotatsu.ui.search.SearchHelper import org.koitharu.kotatsu.ui.settings.AppUpdateService import org.koitharu.kotatsu.ui.settings.SettingsActivity import org.koitharu.kotatsu.ui.tracker.TrackWorker @@ -91,8 +92,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList drawerToggle.onConfigurationChanged(newConfig) } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.opt_main, menu) + menu.findItem(R.id.action_search)?.let { menuItem -> + SearchHelper.setupSearchView(menuItem) + } return super.onCreateOptionsMenu(menu) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchHelper.kt b/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchHelper.kt index ee2cd56fd..aed6383cd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchHelper.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import androidx.appcompat.widget.SearchView import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity import org.koitharu.kotatsu.utils.ext.safe object SearchHelper { @@ -21,12 +22,26 @@ object SearchHelper { view.setOnSuggestionListener(SuggestionListener(view)) } - private class QueryListener(private val context: Context, private val source: MangaSource) : + @JvmStatic + fun setupSearchView(menuItem: MenuItem) { + val view = menuItem.actionView as? SearchView ?: return + val context = view.context + view.queryHint = context.getString(R.string.search_manga) + view.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(context) + view.setOnQueryTextListener(QueryListener(context)) + view.setOnSuggestionListener(SuggestionListener(view)) + } + + private class QueryListener(private val context: Context, private val source: MangaSource? = null) : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { return if (!query.isNullOrBlank()) { - context.startActivity(SearchActivity.newIntent(context, source, query.trim())) + if (source == null) { + context.startActivity(GlobalSearchActivity.newIntent(context, query.trim())) + } else { + context.startActivity(SearchActivity.newIntent(context, source, query.trim())) + } MangaSuggestionsProvider.saveQuery(context, query) true } else false diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchPresenter.kt index 1c74ea3c4..1c436fdbc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchPresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/search/SearchPresenter.kt @@ -15,13 +15,6 @@ import org.koitharu.kotatsu.ui.list.MangaListView @InjectViewState class SearchPresenter : BasePresenter>() { - private lateinit var sources: Array - - override fun onFirstViewAttach() { - sources = MangaSource.values() - super.onFirstViewAttach() - } - fun loadList(source: MangaSource, query: String, offset: Int) { presenterScope.launch { viewState.onLoadingStateChanged(true) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchActivity.kt new file mode 100644 index 000000000..0c5ce0822 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchActivity.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.ui.search.global + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.ui.common.BaseActivity + +class GlobalSearchActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_search) + val query = intent.getStringExtra(EXTRA_QUERY) + + if (query == null) { + finish() + return + } + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + title = query + supportActionBar?.subtitle = getString(R.string.search_results) + supportFragmentManager + .beginTransaction() + .replace(R.id.container, GlobalSearchFragment.newInstance(query)) + .commit() + } + + companion object { + + private const val EXTRA_QUERY = "query" + + fun newIntent(context: Context, query: String) = + Intent(context, GlobalSearchActivity::class.java) + .putExtra(EXTRA_QUERY, query) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchFragment.kt new file mode 100644 index 000000000..5a08335ba --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchFragment.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.ui.search.global + +import moxy.ktx.moxyPresenter +import org.koitharu.kotatsu.ui.list.MangaListFragment +import org.koitharu.kotatsu.utils.ext.withArgs + + +class GlobalSearchFragment: MangaListFragment() { + + private val presenter by moxyPresenter(factory = ::GlobalSearchPresenter) + + private val query by stringArg(ARG_QUERY) + + override fun onRequestMoreItems(offset: Int) { + if (offset == 0) { + presenter.startSearch(query.orEmpty()) + } + } + + override fun getTitle(): CharSequence? { + return query + } + + companion object { + + private const val ARG_QUERY = "query" + + fun newInstance(query: String) = GlobalSearchFragment().withArgs(1) { + putString(ARG_QUERY, query) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchPresenter.kt new file mode 100644 index 000000000..7227fa02e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchPresenter.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.ui.search.global + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.launch +import moxy.presenterScope +import org.koitharu.kotatsu.domain.MangaSearchRepository +import org.koitharu.kotatsu.ui.common.BasePresenter +import org.koitharu.kotatsu.ui.list.MangaListView +import org.koitharu.kotatsu.utils.ext.onFirst +import java.io.IOException + +class GlobalSearchPresenter : BasePresenter>() { + + private lateinit var repository: MangaSearchRepository + + override fun onFirstViewAttach() { + repository = MangaSearchRepository() + super.onFirstViewAttach() + } + + @Suppress("EXPERIMENTAL_API_USAGE") + fun startSearch(query: String) { + presenterScope.launch { + viewState.onLoadingStateChanged(isLoading = true) + repository.globalSearch(query) + .flowOn(Dispatchers.IO) + .catch { e -> + if (e is IOException) { + viewState.onError(e) + } + } + .onFirst { + viewState.onLoadingStateChanged(isLoading = false) + } + .onEmpty { + viewState.onListChanged(emptyList()) + viewState.onLoadingStateChanged(isLoading = false) + } + .collect { + viewState.onListAppended(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt index 67deab49f..c99cf4630 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt @@ -1,5 +1,7 @@ package org.koitharu.kotatsu.utils.ext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.Call import okhttp3.Callback @@ -25,4 +27,14 @@ suspend fun Call.await() = suspendCancellableCoroutine { cont -> this.cancel() } } +} + +fun Flow.onFirst(action: suspend (T) -> Unit): Flow { + var isFirstCall = true + return onEach { + if (isFirstCall) { + action(it) + isFirstCall = false + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_progress.xml b/app/src/main/res/layout/item_progress.xml new file mode 100644 index 000000000..2ca64e856 --- /dev/null +++ b/app/src/main/res/layout/item_progress.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_main.xml b/app/src/main/res/menu/opt_main.xml index 1fe7aa6d4..e7c152f16 100644 --- a/app/src/main/res/menu/opt_main.xml +++ b/app/src/main/res/menu/opt_main.xml @@ -1,2 +1,14 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_remote.xml b/app/src/main/res/menu/opt_remote.xml index 82a1adbcc..598d68e1d 100644 --- a/app/src/main/res/menu/opt_remote.xml +++ b/app/src/main/res/menu/opt_remote.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> Прочитать позже Обновления Here you will see manga updates + Результаты поиска \ 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 d9f3e96f7..526a8e6c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,4 +138,5 @@ Read later Updates Here you will see manga updates + Search results \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1b7a78987..08e69a267 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0-alpha09' + classpath 'com.android.tools.build:gradle:4.1.0-alpha10' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5ac04b8e9..8a314251c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat May 16 07:20:00 EEST 2020 +#Sat May 30 09:45:56 EEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-milestone-1-all.zip