From 786914b1a6a0576d65c32fb373b844eec09637db Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 10 Mar 2022 19:49:40 +0200 Subject: [PATCH 001/213] Shikimori authorization --- app/src/main/AndroidManifest.xml | 6 ++ .../java/org/koitharu/kotatsu/KotatsuApp.kt | 2 + .../kotatsu/core/network/CommonHeaders.kt | 1 + .../kotatsu/core/prefs/AppSettings.kt | 1 + .../reader/ui/SimpleSettingsActivity.kt | 15 ++++ .../kotatsu/settings/MainSettingsFragment.kt | 29 ++++++++ .../kotatsu/shikimori/ShikimoriModule.kt | 31 ++++++++ .../shikimori/data/ShikimoriAuthenticator.kt | 46 ++++++++++++ .../shikimori/data/ShikimoriInterceptor.kt | 19 +++++ .../shikimori/data/ShikimoriRepository.kt | 51 +++++++++++++ .../shikimori/data/ShikimoriStorage.kt | 21 ++++++ .../shikimori/data/model/ShikimoriUser.kt | 16 ++++ .../shikimori/ui/ShikimoriSettingsFragment.kt | 73 +++++++++++++++++++ .../ui/ShikimoriSettingsViewModel.kt | 40 ++++++++++ .../kotatsu/utils/PreferenceIconTarget.kt | 22 ++++++ app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/pref_main.xml | 6 ++ app/src/main/res/xml/pref_shikimori.xml | 13 ++++ 18 files changed, 394 insertions(+) create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriInterceptor.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/PreferenceIconTarget.kt create mode 100644 app/src/main/res/xml/pref_shikimori.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cb22f131e..d177b69df 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,6 +61,12 @@ + + + + + + () { @@ -27,6 +32,7 @@ class SimpleSettingsActivity : BaseActivity() { R.id.container, when (intent?.action) { Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment() + Intent.ACTION_VIEW -> handleUri(intent.data) ?: return ACTION_READER -> ReaderSettingsFragment() ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() ACTION_SOURCE -> SourceSettingsFragment.newInstance( @@ -50,6 +56,15 @@ class SimpleSettingsActivity : BaseActivity() { } } + private fun handleUri(uri: Uri?): Fragment? { + when (uri?.host) { + HOST_SHIKIMORI_AUTH -> return ShikimoriSettingsFragment + .newInstance(authCode = uri.getQueryParameter("code")) + } + finishAfterTransition() + return null + } + companion object { private const val ACTION_READER = diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt index 7a2b992ae..e25b01028 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings import android.content.Intent import android.content.SharedPreferences +import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuInflater @@ -12,6 +13,8 @@ import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceScreen import androidx.preference.TwoStatePreference +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch import leakcanary.LeakCanary import org.koin.android.ext.android.inject @@ -24,6 +27,7 @@ import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.settings.utils.SliderPreference +import org.koitharu.kotatsu.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.names import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat @@ -37,6 +41,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), StorageSelectDialog.OnStorageSelectListener { private val storageManager by inject() + private val shikimoriRepository by inject(mode = LazyThreadSafetyMode.NONE) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -165,6 +170,14 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), } true } + AppSettings.KEY_SHIKIMORI -> { + if (!shikimoriRepository.isAuthorized) { + showShikimoriDialog() + true + } else { + super.onPreferenceTreeClick(preference) + } + } else -> super.onPreferenceTreeClick(preference) } } @@ -179,4 +192,20 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), summary = storage?.getStorageName(context) ?: getString(R.string.not_available) } } + + private fun showShikimoriDialog() { + MaterialAlertDialogBuilder(context ?: return) + .setTitle(R.string.shikimori) + .setMessage(R.string.shikimori_info) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.sign_in) { _, _ -> + runCatching { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(shikimoriRepository.oauthUrl) + startActivity(intent) + }.onFailure { + Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_LONG).show() + } + }.show() + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt new file mode 100644 index 000000000..1fd1d2b05 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.shikimori + +import okhttp3.OkHttpClient +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.network.CurlLoggingInterceptor +import org.koitharu.kotatsu.shikimori.data.ShikimoriAuthenticator +import org.koitharu.kotatsu.shikimori.data.ShikimoriInterceptor +import org.koitharu.kotatsu.shikimori.data.ShikimoriRepository +import org.koitharu.kotatsu.shikimori.data.ShikimoriStorage +import org.koitharu.kotatsu.shikimori.ui.ShikimoriSettingsViewModel + +val shikimoriModule + get() = module { + single { ShikimoriStorage(androidContext()) } + factory { + val okHttp = OkHttpClient.Builder().apply { + authenticator(ShikimoriAuthenticator(get(), ::get)) + addInterceptor(ShikimoriInterceptor(get())) + if (BuildConfig.DEBUG) { + addNetworkInterceptor(CurlLoggingInterceptor()) + } + }.build() + ShikimoriRepository(okHttp, get()) + } + viewModel { params -> + ShikimoriSettingsViewModel(get(), params.getOrNull()) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt new file mode 100644 index 000000000..b62d5bff0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.shikimori.data + +import kotlinx.coroutines.runBlocking +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import org.koitharu.kotatsu.core.network.CommonHeaders + +class ShikimoriAuthenticator( + private val storage: ShikimoriStorage, + private val repositoryProvider: () -> ShikimoriRepository, +) : Authenticator { + + override fun authenticate(route: Route?, response: Response): Request? { + val accessToken = storage.accessToken ?: return null + if (!isRequestWithAccessToken(response)) { + return null; + } + synchronized (this) { + val newAccessToken = storage.accessToken ?: return null + if (accessToken != newAccessToken) { + return newRequestWithAccessToken(response.request, newAccessToken); + } + val updatedAccessToken = refreshAccessToken() ?: return null + return newRequestWithAccessToken(response.request, updatedAccessToken); + } + } + + private fun isRequestWithAccessToken(response: Response): Boolean { + val header = response.request.header(CommonHeaders.AUTHORIZATION) + return header?.startsWith("Bearer") == true + } + + private fun newRequestWithAccessToken(request: Request, accessToken: String): Request { + return request.newBuilder() + .header(CommonHeaders.AUTHORIZATION, "Bearer $accessToken") + .build() + } + + private fun refreshAccessToken(): String? { + val repository = repositoryProvider() + runBlocking { repository.authorize(null) } + return storage.accessToken + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriInterceptor.kt new file mode 100644 index 000000000..33ff454c3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriInterceptor.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.shikimori.data + +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.core.network.CommonHeaders + +private const val USER_AGENT_SHIKIMORI = "Kotatsu" + +class ShikimoriInterceptor(private val storage: ShikimoriStorage) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request().newBuilder() + request.header(CommonHeaders.USER_AGENT, USER_AGENT_SHIKIMORI) + storage.accessToken?.let { + request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") + } + return chain.proceed(request.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt new file mode 100644 index 000000000..cd2f3ad8d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt @@ -0,0 +1,51 @@ +package org.koitharu.kotatsu.shikimori.data + +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser +import org.koitharu.kotatsu.utils.ext.await +import org.koitharu.kotatsu.utils.ext.parseJson + +private const val CLIENT_ID = "Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU" +private const val CLIENT_SECRET = "euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8" +private const val REDIRECT_URI = "kotatsu://shikimori-auth" + +class ShikimoriRepository( + private val okHttp: OkHttpClient, + private val storage: ShikimoriStorage, +) { + + val oauthUrl: String + get() = "https://shikimori.one/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=" + + val isAuthorized: Boolean + get() = storage.accessToken != null + + suspend fun authorize(code: String?) { + val body = FormBody.Builder() + body.add("grant_type", "authorization_code") + body.add("client_id", CLIENT_ID) + body.add("client_secret", CLIENT_SECRET) + if (code != null) { + body.add("redirect_uri", REDIRECT_URI) + body.add("code", code) + } else { + body.add("refresh_token", checkNotNull(storage.refreshToken)) + } + val request = Request.Builder() + .post(body.build()) + .url("https://shikimori.one/oauth/token") + val response = okHttp.newCall(request.build()).await().parseJson() + storage.accessToken = response.getString("access_token") + storage.refreshToken = response.getString("refresh_token") + } + + suspend fun getUser(): ShikimoriUser { + val request = Request.Builder() + .get() + .url("https://shikimori.one/api/users/whoami") + val response = okHttp.newCall(request.build()).await().parseJson() + return ShikimoriUser(response) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt new file mode 100644 index 000000000..2edc946ba --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt @@ -0,0 +1,21 @@ +package org.koitharu.kotatsu.shikimori.data + +import android.content.Context +import androidx.core.content.edit + +private const val PREF_NAME = "shikimori" +private const val KEY_ACCESS_TOKEN = "access_token" +private const val KEY_REFRESH_TOKEN = "refresh_token" + +class ShikimoriStorage(context: Context) { + + private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + + var accessToken: String? + get() = prefs.getString(KEY_ACCESS_TOKEN, null) + set(value) = prefs.edit { putString(KEY_ACCESS_TOKEN, value) } + + var refreshToken: String? + get() = prefs.getString(KEY_REFRESH_TOKEN, null) + set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt new file mode 100644 index 000000000..eff1464e6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.shikimori.data.model + +import org.json.JSONObject + +class ShikimoriUser( + val id: Long, + val nickname: String, + val avatar: String, +) { + + constructor(json: JSONObject) : this( + id = json.getLong("id"), + nickname = json.getString("nickname"), + avatar = json.getString("avatar"), + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt new file mode 100644 index 000000000..ed4e3a600 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt @@ -0,0 +1,73 @@ +package org.koitharu.kotatsu.shikimori.ui + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import coil.ImageLoader +import coil.request.ImageRequest +import coil.transform.CircleCropTransformation +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser +import org.koitharu.kotatsu.utils.PreferenceIconTarget +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.withArgs + +private const val KEY_USER = "shiki_user" + +class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) { + + private val viewModel by viewModel { + parametersOf(arguments?.getString(ARG_AUTH_CODE)) + } + private val coil by inject(mode = LazyThreadSafetyMode.NONE) + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_shikimori) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.user.observe(viewLifecycleOwner, this::onUserChanged) + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + KEY_USER -> openAuthorization() + else -> super.onPreferenceTreeClick(preference) + } + } + + private fun onUserChanged(user: ShikimoriUser?) { + val pref = findPreference(KEY_USER) ?: return + pref.isSelectable = user == null + pref.title = user?.nickname ?: getString(R.string.sign_in) + ImageRequest.Builder(requireContext()) + .data(user?.avatar) + .transformations(CircleCropTransformation()) + .target(PreferenceIconTarget(pref)) + .enqueueWith(coil) + } + + private fun openAuthorization(): Boolean { + return runCatching { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(viewModel.authorizationUrl) + startActivity(intent) + }.isSuccess + } + + companion object { + + private const val ARG_AUTH_CODE = "auth_code" + + fun newInstance(authCode: String?) = ShikimoriSettingsFragment().withArgs(1) { + putString(ARG_AUTH_CODE, authCode) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt new file mode 100644 index 000000000..3fd4186d5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt @@ -0,0 +1,40 @@ +package org.koitharu.kotatsu.shikimori.ui + +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.Dispatchers +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.shikimori.data.ShikimoriRepository +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser + +class ShikimoriSettingsViewModel( + private val repository: ShikimoriRepository, + authCode: String?, +) : BaseViewModel() { + + val authorizationUrl: String + get() = repository.oauthUrl + + val user = MutableLiveData() + + init { + if (authCode != null) { + authorize(authCode) + } else { + loadUser() + } + } + + private fun loadUser() = launchJob(Dispatchers.Default) { + val userModel = if (repository.isAuthorized) { + repository.getUser() + } else { + null + } + user.postValue(userModel) + } + + private fun authorize(code: String) = launchJob(Dispatchers.Default) { + repository.authorize(code) + user.postValue(repository.getUser()) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/PreferenceIconTarget.kt b/app/src/main/java/org/koitharu/kotatsu/utils/PreferenceIconTarget.kt new file mode 100644 index 000000000..edece17d7 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/PreferenceIconTarget.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.utils + +import android.graphics.drawable.Drawable +import androidx.preference.Preference +import coil.target.Target + +class PreferenceIconTarget( + private val preference: Preference, +) : Target { + + override fun onError(error: Drawable?) { + preference.icon = error + } + + override fun onStart(placeholder: Drawable?) { + preference.icon = placeholder + } + + override fun onSuccess(result: Drawable) { + preference.icon = result + } +} \ 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 a5dc3b73a..139c22abe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -266,4 +266,6 @@ Always Preload pages Logged in as %s + Shikimori + Sign in into your Shikimori account to get more features \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 0f4d7d6e3..9b2770caa 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -95,6 +95,12 @@ android:title="@string/check_for_new_chapters" app:iconSpaceReserved="false" /> + + + + + + + From 9d1c4bd66016de1b06ee587bc42f7b1667df8877 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 11 Apr 2022 13:17:26 +0300 Subject: [PATCH 002/213] Add more shikimori api methods --- .../koitharu/kotatsu/details/DetailsModule.kt | 2 +- .../kotatsu/details/ui/DetailsViewModel.kt | 20 +++++++++ .../shikimori/data/ShikimoriRepository.kt | 44 ++++++++++++++++++- .../shikimori/data/model/ShikimoriManga.kt | 44 +++++++++++++++++++ .../data/model/ShikimoriMangaInfo.kt | 20 +++++++++ 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriMangaInfo.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt b/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt index 7e3bd8622..916b75de1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt @@ -8,6 +8,6 @@ val detailsModule get() = module { viewModel { intent -> - DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get()) + DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 7c5fe8574..722a5bf1a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.details.ui import androidx.core.os.LocaleListCompat +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope @@ -26,6 +27,8 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase +import org.koitharu.kotatsu.shikimori.data.ShikimoriRepository +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.iterator @@ -39,6 +42,7 @@ class DetailsViewModel( private val trackingRepository: TrackingRepository, private val mangaDataRepository: MangaDataRepository, private val settings: AppSettings, + private val shikimoriRepository: ShikimoriRepository, ) : BaseViewModel() { private var loadingJob: Job @@ -85,6 +89,7 @@ class DetailsViewModel( .asLiveData(viewModelScope.coroutineContext) val onMangaRemoved = SingleLiveEvent() + val shikimoriInfo = MutableLiveData() val branches = mangaData.map { it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty() @@ -204,6 +209,7 @@ class DetailsViewModel( localMangaRepository.findSavedManga(manga) } }.getOrNull() + findShikimoriManga(manga) } private fun mapChapters( @@ -312,4 +318,18 @@ class DetailsViewModel( it.chapter.name.contains(query, ignoreCase = true) } } + + private fun findShikimoriManga(manga: Manga) { + if (!shikimoriRepository.isAuthorized) { + return + } + launchJob(Dispatchers.Default) { + val data = runCatching { + shikimoriRepository.findMangaInfo(manga) + }.getOrNull() + if (data != null) { + shikimoriInfo.postValue(data) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt index 65a2f828c..c4ae8d251 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt @@ -3,8 +3,11 @@ package org.koitharu.kotatsu.shikimori.data import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request -import org.koitharu.kotatsu.parsers.util.await -import org.koitharu.kotatsu.parsers.util.parseJson +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.util.json.mapJSON +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser private const val CLIENT_ID = "Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU" @@ -49,4 +52,41 @@ class ShikimoriRepository( val response = okHttp.newCall(request.build()).await().parseJson() return ShikimoriUser(response) } + + suspend fun findMangaInfo(manga: Manga): ShikimoriMangaInfo? { + val q = manga.title.urlEncoded() + val request = Request.Builder() + .get() + .url("https://shikimori.one/api/mangas?limit=20&search=$q&censored=false") + val response = okHttp.newCall(request.build()).await().parseJsonArray() + val candidates = response.mapJSON { ShikimoriManga(it) } + val bestCandidate = candidates.minByOrNull { + it.name.levenshteinDistance(manga.title) + } ?: return null + return getMangaInfo(bestCandidate.id) + } + + suspend fun getRelatedManga(id: Long): List { + val request = Request.Builder() + .get() + .url("https://shikimori.one/api/mangas/$id/related") + val response = okHttp.newCall(request.build()).await().parseJsonArray() + return response.mapJSON { jo -> ShikimoriManga(jo) } + } + + suspend fun getSimilarManga(id: Long): List { + val request = Request.Builder() + .get() + .url("https://shikimori.one/api/mangas/$id/similar") + val response = okHttp.newCall(request.build()).await().parseJsonArray() + return response.mapJSON { jo -> ShikimoriManga(jo) } + } + + suspend fun getMangaInfo(id: Long): ShikimoriMangaInfo { + val request = Request.Builder() + .get() + .url("https://shikimori.one/api/mangas/$id") + val response = okHttp.newCall(request.build()).await().parseJson() + return ShikimoriMangaInfo(response) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt new file mode 100644 index 000000000..8ac0411fb --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt @@ -0,0 +1,44 @@ +package org.koitharu.kotatsu.shikimori.data.model + +import org.json.JSONObject + +class ShikimoriManga( + val id: Long, + val name: String, + val cover: String, + val url: String, +) { + + constructor(json: JSONObject) : this( + id = json.getLong("id"), + name = json.getString("name"), + cover = json.getJSONObject("image").getString("preview"), + url = json.getString("url"), + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ShikimoriManga + + if (id != other.id) return false + if (name != other.name) return false + if (cover != other.cover) return false + if (url != other.url) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + cover.hashCode() + result = 31 * result + url.hashCode() + return result + } + + override fun toString(): String { + return "ShikimoriManga #$id \"$name\" $url" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriMangaInfo.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriMangaInfo.kt new file mode 100644 index 000000000..22adad008 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriMangaInfo.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.shikimori.data.model + +import org.json.JSONObject + +class ShikimoriMangaInfo( + val id: Long, + val name: String, + val cover: String, + val url: String, + val descriptionHtml: String, +) { + + constructor(json: JSONObject) : this( + id = json.getLong("id"), + name = json.getString("name"), + cover = json.getJSONObject("image").getString("preview"), + url = json.getString("url"), + descriptionHtml = json.getString("description_html"), + ) +} \ No newline at end of file From f61497ffd906087e353c2a01e0e6aec16fb108c6 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 9 May 2022 12:57:38 +0300 Subject: [PATCH 003/213] Shikimori manga tracking selection list --- app/build.gradle | 2 +- .../kotatsu/base/ui/BaseBottomSheet.kt | 9 ++ .../kotatsu/details/ui/DetailsActivity.kt | 20 ++-- .../kotatsu/details/ui/DetailsFragment.kt | 1 + .../kotatsu/details/ui/DetailsViewModel.kt | 20 +--- .../kotatsu/settings/SettingsActivity.kt | 6 ++ .../kotatsu/shikimori/ShikimoriModule.kt | 2 + .../shikimori/data/ShikimoriAuthenticator.kt | 8 +- .../shikimori/data/ShikimoriRepository.kt | 32 +++++- .../shikimori/data/model/ShikimoriManga.kt | 13 ++- .../shikimori/ui/ShikimoriSettingsFragment.kt | 4 +- .../selector/ShikimoriSelectorBottomSheet.kt | 97 +++++++++++++++++++ .../ui/selector/ShikimoriSelectorViewModel.kt | 69 +++++++++++++ .../ui/selector/adapter/ShikimoriMangaAD.kt | 52 ++++++++++ .../adapter/ShikimoriSelectorAdapter.kt | 40 ++++++++ .../org/koitharu/kotatsu/utils/ext/CoilExt.kt | 2 +- .../res/layout-w600dp/fragment_details.xml | 3 +- app/src/main/res/layout/fragment_details.xml | 3 +- .../main/res/layout/sheet_shiki_selector.xml | 46 +++++++++ app/src/main/res/menu/opt_details.xml | 6 ++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_content.xml | 12 +-- 24 files changed, 398 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriMangaAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriSelectorAdapter.kt create mode 100644 app/src/main/res/layout/sheet_shiki_selector.xml diff --git a/app/build.gradle b/app/build.gradle index 913934c49..ddd401a15 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,7 +66,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:b495e5e457') { + implementation('com.github.nv95:kotatsu-parsers:44e6842025') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt index 75503afc5..647cd93ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt @@ -49,6 +49,15 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { } } + fun addBottomSheetCallback(callback: BottomSheetBehavior.BottomSheetCallback) { + val b = behavior ?: return + b.addBottomSheetCallback(callback) + val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet) + if (rootView != null) { + callback.onStateChanged(rootView, b.state) + } + } + protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index a1920bf80..04363de41 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -45,6 +45,7 @@ import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity +import org.koitharu.kotatsu.shikimori.ui.selector.ShikimoriSelectorBottomSheet import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -151,14 +152,11 @@ class DetailsActivity : override fun onPrepareOptionsMenu(menu: Menu): Boolean { val manga = viewModel.manga.value - menu.findItem(R.id.action_save).isVisible = - manga?.source != null && manga.source != MangaSource.LOCAL - menu.findItem(R.id.action_delete).isVisible = - manga?.source == MangaSource.LOCAL - menu.findItem(R.id.action_browser).isVisible = - manga?.source != MangaSource.LOCAL - menu.findItem(R.id.action_shortcut).isVisible = - ShortcutManagerCompat.isRequestPinShortcutSupported(this) + menu.findItem(R.id.action_save).isVisible = manga?.source != null && manga.source != MangaSource.LOCAL + menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL + menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL + menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(this) + menu.findItem(R.id.action_shiki_track).isVisible = viewModel.isShikimoriAvailable return super.onPrepareOptionsMenu(menu) } @@ -209,6 +207,12 @@ class DetailsActivity : } true } + R.id.action_shiki_track -> { + viewModel.manga.value?.let { + ShikimoriSelectorBottomSheet.show(supportFragmentManager, it) + } + true + } R.id.action_shortcut -> { viewModel.manga.value?.let { lifecycleScope.launch { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 99c7319e3..7aa5d96a0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -34,6 +34,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.SearchActivity +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.ext.* diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 2f242ead6..d5ab2e2c2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.details.ui import androidx.core.os.LocaleListCompat -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope @@ -28,7 +27,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.shikimori.data.ShikimoriRepository -import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -90,7 +88,8 @@ class DetailsViewModel( .asLiveData(viewModelScope.coroutineContext) val onMangaRemoved = SingleLiveEvent() - val shikimoriInfo = MutableLiveData() + val isShikimoriAvailable: Boolean + get() = shikimoriRepository.isAuthorized val branches = mangaData.map { it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty() @@ -216,7 +215,6 @@ class DetailsViewModel( }.onFailure { error -> if (BuildConfig.DEBUG) error.printStackTrace() }.getOrNull() - findShikimoriManga(manga) } private fun mapChapters( @@ -329,18 +327,4 @@ class DetailsViewModel( it.chapter.name.contains(query, ignoreCase = true) } } - - private fun findShikimoriManga(manga: Manga) { - if (!shikimoriRepository.isAuthorized) { - return - } - launchJob(Dispatchers.Default) { - val data = runCatching { - shikimoriRepository.findMangaInfo(manga) - }.getOrNull() - if (data != null) { - shikimoriInfo.postValue(data) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt index 75f20b8ff..bce7c3c31 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -121,6 +121,7 @@ class SettingsActivity : Intent.ACTION_VIEW -> handleUri(intent.data) ?: return ACTION_READER -> ReaderSettingsFragment() ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() + ACTION_SHIKIMORI -> ShikimoriSettingsFragment() ACTION_SOURCE -> SourceSettingsFragment.newInstance( intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL ) @@ -146,6 +147,7 @@ class SettingsActivity : private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" private const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS" private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" + private const val ACTION_SHIKIMORI = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SHIKIMORI_SETTINGS" private const val EXTRA_SOURCE = "source" private const val HOST_SHIKIMORI_AUTH = "shikimori-auth" @@ -156,6 +158,10 @@ class SettingsActivity : Intent(context, SettingsActivity::class.java) .setAction(ACTION_READER) + fun newShikimoriSettingsIntent(context: Context) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_SHIKIMORI) + fun newSuggestionsSettingsIntent(context: Context) = Intent(context, SettingsActivity::class.java) .setAction(ACTION_SUGGESTIONS) diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt index 7011ad798..64a2bda32 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ShikimoriModule.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.shikimori.data.ShikimoriInterceptor import org.koitharu.kotatsu.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.shikimori.data.ShikimoriStorage import org.koitharu.kotatsu.shikimori.ui.ShikimoriSettingsViewModel +import org.koitharu.kotatsu.shikimori.ui.selector.ShikimoriSelectorViewModel val shikimoriModule get() = module { @@ -23,4 +24,5 @@ val shikimoriModule viewModel { params -> ShikimoriSettingsViewModel(get(), params.getOrNull()) } + viewModel { params -> ShikimoriSelectorViewModel(params[0], get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt index b62d5bff0..6a73b5829 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt @@ -15,15 +15,15 @@ class ShikimoriAuthenticator( override fun authenticate(route: Route?, response: Response): Request? { val accessToken = storage.accessToken ?: return null if (!isRequestWithAccessToken(response)) { - return null; + return null } - synchronized (this) { + synchronized(this) { val newAccessToken = storage.accessToken ?: return null if (accessToken != newAccessToken) { - return newRequestWithAccessToken(response.request, newAccessToken); + return newRequestWithAccessToken(response.request, newAccessToken) } val updatedAccessToken = refreshAccessToken() ?: return null - return newRequestWithAccessToken(response.request, updatedAccessToken); + return newRequestWithAccessToken(response.request, updatedAccessToken) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt index c4ae8d251..2cfca1505 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt @@ -1,11 +1,15 @@ package org.koitharu.kotatsu.shikimori.data import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.json.mapJSON +import org.koitharu.kotatsu.parsers.util.parseJson +import org.koitharu.kotatsu.parsers.util.parseJsonArray +import org.koitharu.kotatsu.parsers.util.urlEncoded import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser @@ -13,6 +17,8 @@ import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser private const val CLIENT_ID = "Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU" private const val CLIENT_SECRET = "euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8" private const val REDIRECT_URI = "kotatsu://shikimori-auth" +private const val BASE_URL = "https://shikimori.one/api/" +private const val MANGA_PAGE_SIZE = 10 class ShikimoriRepository( private val okHttp: OkHttpClient, @@ -53,16 +59,32 @@ class ShikimoriRepository( return ShikimoriUser(response) } + suspend fun findManga(query: String, offset: Int): List { + val page = offset / MANGA_PAGE_SIZE + val pageOffset = offset % MANGA_PAGE_SIZE + val url = BASE_URL.toHttpUrl().newBuilder() + .addPathSegment("mangas") + .addEncodedQueryParameter("page", (page + 1).toString()) + .addEncodedQueryParameter("limit", MANGA_PAGE_SIZE.toString()) + .addEncodedQueryParameter("censored", false.toString()) + .addQueryParameter("search", query) + .build() + val request = Request.Builder().url(url).get().build() + val response = okHttp.newCall(request).await().parseJsonArray() + val list = response.mapJSON { ShikimoriManga(it) } + return if (pageOffset != 0) list.drop(pageOffset) else list + } + suspend fun findMangaInfo(manga: Manga): ShikimoriMangaInfo? { val q = manga.title.urlEncoded() val request = Request.Builder() .get() - .url("https://shikimori.one/api/mangas?limit=20&search=$q&censored=false") + .url("https://shikimori.one/api/mangas?limit=5&search=$q&censored=false") val response = okHttp.newCall(request.build()).await().parseJsonArray() val candidates = response.mapJSON { ShikimoriManga(it) } - val bestCandidate = candidates.minByOrNull { - it.name.levenshteinDistance(manga.title) - } ?: return null + val bestCandidate = candidates.filter { + it.name.equals(manga.title, ignoreCase = true) || it.name.equals(manga.altTitle, ignoreCase = true) + }.singleOrNull() ?: return null return getMangaInfo(bestCandidate.id) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt index 8ac0411fb..0369ea255 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriManga.kt @@ -1,19 +1,24 @@ package org.koitharu.kotatsu.shikimori.data.model import org.json.JSONObject +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.util.json.getStringOrNull +import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl class ShikimoriManga( val id: Long, val name: String, + val altName: String?, val cover: String, val url: String, -) { +) : ListModel { constructor(json: JSONObject) : this( id = json.getLong("id"), name = json.getString("name"), - cover = json.getJSONObject("image").getString("preview"), - url = json.getString("url"), + altName = json.getStringOrNull("russian"), + cover = json.getJSONObject("image").getString("preview").toAbsoluteUrl("shikimori.one"), + url = json.getString("url").toAbsoluteUrl("shikimori.one"), ) override fun equals(other: Any?): Boolean { @@ -24,6 +29,7 @@ class ShikimoriManga( if (id != other.id) return false if (name != other.name) return false + if (altName != other.altName) return false if (cover != other.cover) return false if (url != other.url) return false @@ -33,6 +39,7 @@ class ShikimoriManga( override fun hashCode(): Int { var result = id.hashCode() result = 31 * result + name.hashCode() + result = 31 * result + altName.hashCode() result = 31 * result + cover.hashCode() result = 31 * result + url.hashCode() return result diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt index ed4e3a600..27fa2ed7a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt @@ -18,8 +18,6 @@ import org.koitharu.kotatsu.utils.PreferenceIconTarget import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.withArgs -private const val KEY_USER = "shiki_user" - class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) { private val viewModel by viewModel { @@ -64,6 +62,8 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) { companion object { + private const val KEY_USER = "shiki_user" + private const val ARG_AUTH_CODE = "auth_code" fun newInstance(authCode: String?) = ShikimoriSettingsFragment().withArgs(1) { diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt new file mode 100644 index 000000000..484c71aa3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt @@ -0,0 +1,97 @@ +package org.koitharu.kotatsu.shikimori.ui.selector + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.FragmentManager +import coil.transform.CircleCropTransformation +import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.domain.MangaIntent +import org.koitharu.kotatsu.base.ui.BaseBottomSheet +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.databinding.SheetShikiSelectorBinding +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga +import org.koitharu.kotatsu.shikimori.ui.selector.adapter.ShikimoriSelectorAdapter +import org.koitharu.kotatsu.utils.BottomSheetToolbarController +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.newImageRequest +import org.koitharu.kotatsu.utils.ext.withArgs + +class ShikimoriSelectorBottomSheet : + BaseBottomSheet(), + OnListItemClickListener, + PaginationScrollListener.Callback, View.OnClickListener { + + private val viewModel by viewModel { + parametersOf(requireNotNull(requireArguments().getParcelable(MangaIntent.KEY_MANGA)).manga) + } + + override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetShikiSelectorBinding { + return SheetShikiSelectorBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.toolbar.subtitle = viewModel.manga.title + binding.toolbar.setNavigationOnClickListener { dismiss() } + addBottomSheetCallback(BottomSheetToolbarController(binding.toolbar)) + val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, get(), this) + with(binding.recyclerView) { + adapter = listAdapter + addOnScrollListener(PaginationScrollListener(4, this@ShikimoriSelectorBottomSheet)) + } + binding.imageViewUser.setOnClickListener(this) + + viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it } + viewModel.onError.observe(viewLifecycleOwner, ::onError) + viewModel.avatar.observe(viewLifecycleOwner, ::setUserAvatar) + } + + override fun onClick(v: View) { + when (v.id) { + R.id.imageView_user -> startActivity(SettingsActivity.newShikimoriSettingsIntent(v.context)) + } + } + + override fun onItemClick(item: ShikimoriManga, view: View) { + } + + override fun onScrolledToEnd() { + viewModel.loadList(append = true) + } + + private fun onError(e: Throwable) { + Toast.makeText(requireContext(), e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() + if (viewModel.isEmpty) { + dismissAllowingStateLoss() + } + } + + private fun setUserAvatar(url: String?) { + val iconSize = resources.getDimensionPixelSize(R.dimen.action_bar_item_size) + binding.imageViewUser.newImageRequest(url) + .transformations(CircleCropTransformation()) + .size(iconSize, iconSize) + .enqueueWith(get()) + } + + companion object { + + private const val TAG = "ShikimoriSelectorBottomSheet" + + fun show(fm: FragmentManager, manga: Manga) = + ShikimoriSelectorBottomSheet().withArgs(1) { + putParcelable(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = false)) + }.show(fm, TAG) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt new file mode 100644 index 000000000..7e671f1e0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt @@ -0,0 +1,69 @@ +package org.koitharu.kotatsu.shikimori.ui.selector + +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.shikimori.data.ShikimoriRepository +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga +import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct + +class ShikimoriSelectorViewModel( + val manga: Manga, + private val repository: ShikimoriRepository, +) : BaseViewModel() { + + private val shikiMangaList = MutableStateFlow?>(null) + private val hasNextPage = MutableStateFlow(false) + private var loadingJob: Job? = null + + val content: LiveData> = combine( + shikiMangaList.filterNotNull(), + hasNextPage + ) { list, isHasNextPage -> + when { + list.isEmpty() -> listOf() + isHasNextPage -> list + LoadingFooter + else -> list + } + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + + val avatar = liveData { + emit(runCatching { repository.getUser().avatar }.getOrNull()) + } + + val isEmpty: Boolean + get() = shikiMangaList.value.isNullOrEmpty() + + init { + loadList(append = false) + } + + fun loadList(append: Boolean) { + if (loadingJob?.isActive == true) { + return + } + if (append && !hasNextPage.value) { + return + } + loadingJob = launchLoadingJob(Dispatchers.Default) { + val offset = if (append) shikiMangaList.value?.size ?: 0 else 0 + val list = repository.findManga(manga.title, offset) + if (!append) { + shikiMangaList.value = list + } else if (list.isNotEmpty()) { + shikiMangaList.value = shikiMangaList.value?.plus(list) ?: list + } + hasNextPage.value = list.isNotEmpty() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriMangaAD.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriMangaAD.kt new file mode 100644 index 000000000..4685806c8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriMangaAD.kt @@ -0,0 +1,52 @@ +package org.koitharu.kotatsu.shikimori.ui.selector.adapter + +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import coil.request.Disposable +import coil.size.Scale +import coil.util.CoilUtils +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.databinding.ItemMangaListBinding +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest +import org.koitharu.kotatsu.utils.ext.textAndVisible + +fun shikimoriMangaAD( + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + clickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) } +) { + + var imageRequest: Disposable? = null + + itemView.setOnClickListener { + clickListener.onItemClick(item, it) + } + + bind { + imageRequest?.dispose() + binding.textViewTitle.text = item.name + binding.textViewSubtitle.textAndVisible = item.altName + imageRequest = binding.imageViewCover.newImageRequest(item.cover) + .placeholder(R.drawable.ic_placeholder) + .fallback(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + .scale(Scale.FILL) + .allowRgb565(true) + .lifecycle(lifecycleOwner) + .enqueueWith(coil) + } + + onViewRecycled { + imageRequest?.dispose() + imageRequest = null + CoilUtils.dispose(binding.imageViewCover) + binding.imageViewCover.setImageDrawable(null) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriSelectorAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriSelectorAdapter.kt new file mode 100644 index 000000000..5457aa078 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikimoriSelectorAdapter.kt @@ -0,0 +1,40 @@ +package org.koitharu.kotatsu.shikimori.ui.selector.adapter + +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD +import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga +import kotlin.jvm.internal.Intrinsics + +class ShikimoriSelectorAdapter( + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + clickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(loadingStateAD()) + .addDelegate(shikimoriMangaAD(lifecycleOwner, coil, clickListener)) + .addDelegate(loadingFooterAD()) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return when { + oldItem === newItem -> true + oldItem is ShikimoriManga && newItem is ShikimoriManga -> oldItem.id == newItem.id + else -> false + } + } + + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return Intrinsics.areEqual(oldItem, newItem) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt index 53cf3bdb2..16416bf51 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt @@ -11,7 +11,7 @@ import com.google.android.material.progressindicator.BaseProgressIndicator import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener -fun ImageView.newImageRequest(url: String) = ImageRequest.Builder(context) +fun ImageView.newImageRequest(url: String?) = ImageRequest.Builder(context) .data(url) .crossfade(true) .target(this) diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml index 936a0ac6c..17cb204f4 100644 --- a/app/src/main/res/layout-w600dp/fragment_details.xml +++ b/app/src/main/res/layout-w600dp/fragment_details.xml @@ -160,7 +160,7 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_details.xml b/app/src/main/res/menu/opt_details.xml index 97236c2f0..d6cc9be85 100644 --- a/app/src/main/res/menu/opt_details.xml +++ b/app/src/main/res/menu/opt_details.xml @@ -17,6 +17,12 @@ android:visible="false" app:showAsAction="never" /> + + Название Изменить Изменить категорию + Отслеживание \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 282d15034..461b93d8b 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -21,6 +21,8 @@ 16dp 2dp 12dp + 24dp + 32dp 124dp 4dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7199a5b89..aca50b55e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -289,4 +289,5 @@ Name Edit Edit category + Tracking \ No newline at end of file diff --git a/app/src/main/res/xml/pref_content.xml b/app/src/main/res/xml/pref_content.xml index 178cec046..0c0400bf2 100644 --- a/app/src/main/res/xml/pref_content.xml +++ b/app/src/main/res/xml/pref_content.xml @@ -11,9 +11,13 @@ + + - - Date: Mon, 9 May 2022 16:24:24 +0300 Subject: [PATCH 004/213] Cache shikimori user --- .../decor/AbstractSelectionItemDecoration.kt | 2 +- .../kotatsu/core/prefs/AppSettings.kt | 11 +--- .../list/ui/MangaSelectionDecoration.kt | 14 ++--- .../shikimori/data/ShikimoriAuthenticator.kt | 9 +++- .../shikimori/data/ShikimoriRepository.kt | 49 +++++++++++++---- .../shikimori/data/ShikimoriStorage.kt | 15 ++++++ .../shikimori/data/model/ShikimoriUser.kt | 26 ++++++++++ .../shikimori/ui/ShikimoriSettingsFragment.kt | 6 +++ .../ui/ShikimoriSettingsViewModel.kt | 8 +++ .../selector/ShikimoriSelectorBottomSheet.kt | 11 +++- .../ui/selector/ShikimoriSelectorViewModel.kt | 7 ++- .../adapter/ShikiMangaSelectionDecoration.kt | 52 +++++++++++++++++++ .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 30 ++++++++++- .../org/koitharu/kotatsu/utils/ext/HttpExt.kt | 10 ++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_shikimori.xml | 6 +++ 16 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt index ac624d3c6..1974f6a5d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt @@ -12,7 +12,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() { private val bounds = Rect() private val boundsF = RectF() - private val selection = HashSet() + protected val selection = HashSet() protected var hasBackground: Boolean = true protected var hasForeground: Boolean = false diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index ac70b392a..a3ebc7f7a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -22,6 +22,7 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.utils.ext.getEnumValue +import org.koitharu.kotatsu.utils.ext.observe import org.koitharu.kotatsu.utils.ext.putEnumValue import org.koitharu.kotatsu.utils.ext.toUriOrNull @@ -236,15 +237,7 @@ class AppSettings(context: Context) { prefs.unregisterOnSharedPreferenceChangeListener(listener) } - fun observe() = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - trySendBlocking(key) - } - prefs.registerOnSharedPreferenceChangeListener(listener) - awaitClose { - prefs.unregisterOnSharedPreferenceChangeListener(listener) - } - } + fun observe() = prefs.observe() companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt index 3ef530824..8422d00ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt @@ -18,17 +18,17 @@ import org.koitharu.kotatsu.utils.ext.getItem import org.koitharu.kotatsu.utils.ext.getThemeColor import com.google.android.material.R as materialR -class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() { +open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() { - private val paint = Paint(Paint.ANTI_ALIAS_FLAG) - private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle) - private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer) - private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED) - private val fillColor = ColorUtils.setAlphaComponent( + protected val paint = Paint(Paint.ANTI_ALIAS_FLAG) + protected val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle) + protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer) + protected val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED) + protected val fillColor = ColorUtils.setAlphaComponent( ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f), 0x74 ) - private val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner) + protected val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner) init { hasBackground = false diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt index 6a73b5829..6bf1381c2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriAuthenticator.kt @@ -5,6 +5,7 @@ import okhttp3.Authenticator import okhttp3.Request import okhttp3.Response import okhttp3.Route +import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.network.CommonHeaders class ShikimoriAuthenticator( @@ -38,9 +39,13 @@ class ShikimoriAuthenticator( .build() } - private fun refreshAccessToken(): String? { + private fun refreshAccessToken(): String? = runCatching { val repository = repositoryProvider() runBlocking { repository.authorize(null) } return storage.accessToken - } + }.onFailure { + if (BuildConfig.DEBUG) { + it.printStackTrace() + } + }.getOrNull() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt index 2cfca1505..5accdf614 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt @@ -4,6 +4,7 @@ import okhttp3.FormBody import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request +import org.json.JSONObject import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.json.mapJSON @@ -13,11 +14,12 @@ import org.koitharu.kotatsu.parsers.util.urlEncoded import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser +import org.koitharu.kotatsu.utils.ext.toRequestBody private const val CLIENT_ID = "Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU" private const val CLIENT_SECRET = "euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8" private const val REDIRECT_URI = "kotatsu://shikimori-auth" -private const val BASE_URL = "https://shikimori.one/api/" +private const val BASE_URL = "https://shikimori.one/" private const val MANGA_PAGE_SIZE = 10 class ShikimoriRepository( @@ -26,7 +28,7 @@ class ShikimoriRepository( ) { val oauthUrl: String - get() = "https://shikimori.one/oauth/authorize?client_id=$CLIENT_ID&" + + get() = "${BASE_URL}oauth/authorize?client_id=$CLIENT_ID&" + "redirect_uri=$REDIRECT_URI&response_type=code&scope=" val isAuthorized: Boolean @@ -45,7 +47,7 @@ class ShikimoriRepository( } val request = Request.Builder() .post(body.build()) - .url("https://shikimori.one/oauth/token") + .url("${BASE_URL}oauth/token") val response = okHttp.newCall(request.build()).await().parseJson() storage.accessToken = response.getString("access_token") storage.refreshToken = response.getString("refresh_token") @@ -54,15 +56,24 @@ class ShikimoriRepository( suspend fun getUser(): ShikimoriUser { val request = Request.Builder() .get() - .url("https://shikimori.one/api/users/whoami") + .url("${BASE_URL}api/users/whoami") val response = okHttp.newCall(request.build()).await().parseJson() - return ShikimoriUser(response) + return ShikimoriUser(response).also { storage.user = it } + } + + fun getCachedUser(): ShikimoriUser? { + return storage.user + } + + fun logout() { + storage.clear() } suspend fun findManga(query: String, offset: Int): List { val page = offset / MANGA_PAGE_SIZE val pageOffset = offset % MANGA_PAGE_SIZE val url = BASE_URL.toHttpUrl().newBuilder() + .addPathSegment("api") .addPathSegment("mangas") .addEncodedQueryParameter("page", (page + 1).toString()) .addEncodedQueryParameter("limit", MANGA_PAGE_SIZE.toString()) @@ -75,11 +86,31 @@ class ShikimoriRepository( return if (pageOffset != 0) list.drop(pageOffset) else list } + suspend fun trackManga(manga: Manga, shikiMangaId: Long) { + val user = getCachedUser() ?: getUser() + val payload = JSONObject() + payload.put( + "user_rate", + JSONObject().apply { + put("target_id", shikiMangaId) + put("target_type", "Manga") + put("user_id", user.id) + } + ) + val url = BASE_URL.toHttpUrl().newBuilder() + .addPathSegment("api") + .addPathSegment("v2") + .addPathSegment("user_rates") + .build() + val request = Request.Builder().url(url).post(payload.toRequestBody()).build() + val response = okHttp.newCall(request).await().parseJson() + } + suspend fun findMangaInfo(manga: Manga): ShikimoriMangaInfo? { val q = manga.title.urlEncoded() val request = Request.Builder() .get() - .url("https://shikimori.one/api/mangas?limit=5&search=$q&censored=false") + .url("${BASE_URL}api/mangas?limit=5&search=$q&censored=false") val response = okHttp.newCall(request.build()).await().parseJsonArray() val candidates = response.mapJSON { ShikimoriManga(it) } val bestCandidate = candidates.filter { @@ -91,7 +122,7 @@ class ShikimoriRepository( suspend fun getRelatedManga(id: Long): List { val request = Request.Builder() .get() - .url("https://shikimori.one/api/mangas/$id/related") + .url("${BASE_URL}api/mangas/$id/related") val response = okHttp.newCall(request.build()).await().parseJsonArray() return response.mapJSON { jo -> ShikimoriManga(jo) } } @@ -99,7 +130,7 @@ class ShikimoriRepository( suspend fun getSimilarManga(id: Long): List { val request = Request.Builder() .get() - .url("https://shikimori.one/api/mangas/$id/similar") + .url("${BASE_URL}api/mangas/$id/similar") val response = okHttp.newCall(request.build()).await().parseJsonArray() return response.mapJSON { jo -> ShikimoriManga(jo) } } @@ -107,7 +138,7 @@ class ShikimoriRepository( suspend fun getMangaInfo(id: Long): ShikimoriMangaInfo { val request = Request.Builder() .get() - .url("https://shikimori.one/api/mangas/$id") + .url("${BASE_URL}api/mangas/$id") val response = okHttp.newCall(request.build()).await().parseJson() return ShikimoriMangaInfo(response) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt index 2edc946ba..210432670 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriStorage.kt @@ -2,10 +2,13 @@ package org.koitharu.kotatsu.shikimori.data import android.content.Context import androidx.core.content.edit +import org.json.JSONObject +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser private const val PREF_NAME = "shikimori" private const val KEY_ACCESS_TOKEN = "access_token" private const val KEY_REFRESH_TOKEN = "refresh_token" +private const val KEY_USER = "user" class ShikimoriStorage(context: Context) { @@ -18,4 +21,16 @@ class ShikimoriStorage(context: Context) { var refreshToken: String? get() = prefs.getString(KEY_REFRESH_TOKEN, null) set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) } + + var user: ShikimoriUser? + get() = prefs.getString(KEY_USER, null)?.let { + ShikimoriUser(JSONObject(it)) + } + set(value) = prefs.edit { + putString(KEY_USER, value?.toJson()?.toString()) + } + + fun clear() = prefs.edit { + clear() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt index eff1464e6..d42188a63 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/model/ShikimoriUser.kt @@ -13,4 +13,30 @@ class ShikimoriUser( nickname = json.getString("nickname"), avatar = json.getString("avatar"), ) + + fun toJson() = JSONObject().apply { + put("id", id) + put("nickname", nickname) + put("avatar", avatar) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ShikimoriUser + + if (id != other.id) return false + if (nickname != other.nickname) return false + if (avatar != other.avatar) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + nickname.hashCode() + result = 31 * result + avatar.hashCode() + return result + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt index 27fa2ed7a..aa7cbc6a4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsFragment.kt @@ -37,6 +37,10 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) { override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { KEY_USER -> openAuthorization() + KEY_LOGOUT -> { + viewModel.logout() + true + } else -> super.onPreferenceTreeClick(preference) } } @@ -50,6 +54,7 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) { .transformations(CircleCropTransformation()) .target(PreferenceIconTarget(pref)) .enqueueWith(coil) + findPreference(KEY_LOGOUT)?.isVisible = user != null } private fun openAuthorization(): Boolean { @@ -63,6 +68,7 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) { companion object { private const val KEY_USER = "shiki_user" + private const val KEY_LOGOUT = "shiki_logout" private const val ARG_AUTH_CODE = "auth_code" diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt index 3fd4186d5..88e62f08d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/ShikimoriSettingsViewModel.kt @@ -24,8 +24,16 @@ class ShikimoriSettingsViewModel( } } + fun logout() { + launchJob(Dispatchers.Default) { + repository.logout() + user.postValue(null) + } + } + private fun loadUser() = launchJob(Dispatchers.Default) { val userModel = if (repository.isAuthorized) { + repository.getCachedUser()?.let(user::postValue) repository.getUser() } else { null diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt index 484c71aa3..ed2cf4997 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorBottomSheet.kt @@ -20,6 +20,7 @@ import org.koitharu.kotatsu.databinding.SheetShikiSelectorBinding import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga +import org.koitharu.kotatsu.shikimori.ui.selector.adapter.ShikiMangaSelectionDecoration import org.koitharu.kotatsu.shikimori.ui.selector.adapter.ShikimoriSelectorAdapter import org.koitharu.kotatsu.utils.BottomSheetToolbarController import org.koitharu.kotatsu.utils.ext.enqueueWith @@ -30,7 +31,8 @@ import org.koitharu.kotatsu.utils.ext.withArgs class ShikimoriSelectorBottomSheet : BaseBottomSheet(), OnListItemClickListener, - PaginationScrollListener.Callback, View.OnClickListener { + PaginationScrollListener.Callback, + View.OnClickListener { private val viewModel by viewModel { parametersOf(requireNotNull(requireArguments().getParcelable(MangaIntent.KEY_MANGA)).manga) @@ -46,13 +48,19 @@ class ShikimoriSelectorBottomSheet : binding.toolbar.setNavigationOnClickListener { dismiss() } addBottomSheetCallback(BottomSheetToolbarController(binding.toolbar)) val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, get(), this) + val decoration = ShikiMangaSelectionDecoration(view.context) with(binding.recyclerView) { adapter = listAdapter + addItemDecoration(decoration) addOnScrollListener(PaginationScrollListener(4, this@ShikimoriSelectorBottomSheet)) } binding.imageViewUser.setOnClickListener(this) viewModel.content.observe(viewLifecycleOwner) { listAdapter.items = it } + viewModel.selectedItemId.observe(viewLifecycleOwner) { + decoration.checkedItemId = it + binding.recyclerView.invalidateItemDecorations() + } viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.avatar.observe(viewLifecycleOwner, ::setUserAvatar) } @@ -64,6 +72,7 @@ class ShikimoriSelectorBottomSheet : } override fun onItemClick(item: ShikimoriManga, view: View) { + viewModel.selectedItemId.value = item.id } override fun onScrolledToEnd() { diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt index 7e671f1e0..4488d0875 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/ShikimoriSelectorViewModel.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.shikimori.ui.selector import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -37,7 +39,10 @@ class ShikimoriSelectorViewModel( } }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) - val avatar = liveData { + val selectedItemId = MutableLiveData(RecyclerView.NO_ID) + + val avatar = liveData(viewModelScope.coroutineContext + Dispatchers.Default) { + emit(repository.getCachedUser()?.avatar) emit(runCatching { repository.getUser().avatar }.getOrNull()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt new file mode 100644 index 000000000..72f758bc3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/ui/selector/adapter/ShikiMangaSelectionDecoration.kt @@ -0,0 +1,52 @@ +package org.koitharu.kotatsu.shikimori.ui.selector.adapter + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.NO_ID +import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration +import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga +import org.koitharu.kotatsu.utils.ext.getItem + +class ShikiMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { + + var checkedItemId: Long + get() = selection.singleOrNull() ?: NO_ID + set(value) { + clearSelection() + if (value != NO_ID) { + selection.add(value) + } + } + + override fun getItemId(parent: RecyclerView, child: View): Long { + val holder = parent.getChildViewHolder(child) ?: return NO_ID + val item = holder.getItem(ShikimoriManga::class.java) ?: return NO_ID + return item.id + } + + override fun onDrawForeground( + canvas: Canvas, + parent: RecyclerView, + child: View, + bounds: RectF, + state: RecyclerView.State, + ) { + paint.color = strokeColor + paint.style = Paint.Style.STROKE + canvas.drawRoundRect(bounds, defaultRadius, defaultRadius, paint) + checkIcon?.run { + val offset = (bounds.height() - intrinsicHeight) / 2 + setBounds( + (bounds.right - offset - intrinsicWidth).toInt(), + (bounds.top + offset).toInt(), + (bounds.right - offset).toInt(), + (bounds.top + offset + intrinsicHeight).toInt(), + ) + draw(canvas) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 6f15f7cd3..c3fdde215 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.utils.ext import android.content.Context +import android.content.SharedPreferences import android.content.pm.ResolveInfo import android.net.ConnectivityManager import android.net.Network @@ -10,8 +11,14 @@ import android.os.Build import androidx.activity.result.ActivityResultLauncher import androidx.core.app.ActivityOptionsCompat import androidx.work.CoroutineWorker -import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.suspendCancellableCoroutine val Context.connectivityManager: ConnectivityManager get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager @@ -55,4 +62,23 @@ fun ActivityResultLauncher.tryLaunch(input: I, options: ActivityOptionsCo return runCatching { launch(input, options) }.isSuccess -} \ No newline at end of file +} + +fun SharedPreferences.observe() = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + trySendBlocking(key) + } + registerOnSharedPreferenceChangeListener(listener) + awaitClose { + unregisterOnSharedPreferenceChangeListener(listener) + } +} + +fun SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow = flow { + emit(valueProducer()) + observe().collect { upstreamKey -> + if (upstreamKey == key) { + emit(valueProducer()) + } + } +}.distinctUntilChanged() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt new file mode 100644 index 000000000..058ca4ea6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt @@ -0,0 +1,10 @@ + +package org.koitharu.kotatsu.utils.ext + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject + +private val TYPE_JSON = "application/json".toMediaType() + +fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON) \ 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 d24c165d3..78a8144e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -291,4 +291,5 @@ Edit category Tracking No favourite categories + Logout \ No newline at end of file diff --git a/app/src/main/res/xml/pref_shikimori.xml b/app/src/main/res/xml/pref_shikimori.xml index b77f72f4f..0e8d9118f 100644 --- a/app/src/main/res/xml/pref_shikimori.xml +++ b/app/src/main/res/xml/pref_shikimori.xml @@ -10,4 +10,10 @@ android:title="@string/loading_" app:iconSpaceReserved="true" /> + + From 5d881ca154a96fb936e7417d11b3abcedfda066b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 17 May 2022 11:10:25 +0300 Subject: [PATCH 005/213] New global search activity --- app/src/main/AndroidManifest.xml | 3 + .../kotatsu/details/ui/DetailsActivity.kt | 4 +- .../list/ui/adapter/MangaGridItemAD.kt | 8 + .../list/ui/adapter/MangaListAdapter.kt | 2 +- .../list/ui/model/ListModelConversionExt.kt | 4 +- .../koitharu/kotatsu/main/ui/MainActivity.kt | 4 +- .../koitharu/kotatsu/search/SearchModule.kt | 10 +- .../kotatsu/search/ui/MangaListActivity.kt | 6 +- .../search/ui/global/GlobalSearchActivity.kt | 55 ------ .../search/ui/global/GlobalSearchFragment.kt | 35 ---- .../search/ui/global/GlobalSearchViewModel.kt | 87 --------- .../search/ui/multi/MultiSearchActivity.kt | 183 ++++++++++++++++++ .../search/ui/multi/MultiSearchListModel.kt | 29 +++ .../search/ui/multi/MultiSearchViewModel.kt | 112 +++++++++++ .../ui/multi/adapter/ItemSizeResolver.kt | 15 ++ .../ui/multi/adapter/MultiSearchAdapter.kt | 59 ++++++ .../ui/multi/adapter/SearchResultsAD.kt | 47 +++++ .../org/koitharu/kotatsu/utils/ext/ViewExt.kt | 18 +- ...arch_global.xml => activity_container.xml} | 0 .../main/res/layout/activity_search_multi.xml | 29 +++ app/src/main/res/layout/item_list_group.xml | 32 +++ app/src/main/res/layout/item_manga_grid.xml | 3 +- 22 files changed, 549 insertions(+), 196 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/ItemSizeResolver.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt rename app/src/main/res/layout/{activity_search_global.xml => activity_container.xml} (100%) create mode 100644 app/src/main/res/layout/activity_search_multi.xml create mode 100644 app/src/main/res/layout/item_list_group.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 13f5cecbf..9cb967580 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -88,6 +88,9 @@ + { viewModel.manga.value?.let { - startActivity(GlobalSearchActivity.newIntent(this, it.title)) + startActivity(MultiSearchActivity.newIntent(this, it.title)) } true } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index f1d6d3af4..e4ad38d3e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.list.ui.adapter +import androidx.core.view.updateLayoutParams import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable @@ -13,6 +14,7 @@ import org.koitharu.kotatsu.databinding.ItemMangaGridBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.search.ui.multi.adapter.ItemSizeResolver import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.referer @@ -21,6 +23,7 @@ fun mangaGridItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, + sizeResolver: ItemSizeResolver?, ) = adapterDelegateViewBinding( { inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) } ) { @@ -34,6 +37,11 @@ fun mangaGridItemAD( itemView.setOnLongClickListener { clickListener.onItemLongClick(item.manga, it) } + if (sizeResolver != null) { + itemView.updateLayoutParams { + width = sizeResolver.cellWidth + } + } bind { binding.textViewTitle.text = item.title diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 2b359a8a9..93f271c0c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -18,7 +18,7 @@ class MangaListAdapter( delegatesManager .addDelegate(ITEM_TYPE_MANGA_LIST, mangaListItemAD(coil, lifecycleOwner, listener)) .addDelegate(ITEM_TYPE_MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, lifecycleOwner, listener)) - .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener)) + .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, null)) .addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD()) .addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD()) .addDelegate(ITEM_TYPE_DATE, relatedDateItemAD()) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt index 0d9de41f3..cda127e73 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt @@ -40,7 +40,7 @@ fun Manga.toGridModel(counter: Int) = MangaGridModel( suspend fun List.toUi( mode: ListMode, countersProvider: CountersProvider, -): List = when (mode) { +): List = when (mode) { ListMode.LIST -> map { it.toListModel(countersProvider.getCounter(it.id)) } ListMode.DETAILED_LIST -> map { it.toListDetailedModel(countersProvider.getCounter(it.id)) } ListMode.GRID -> map { it.toGridModel(countersProvider.getCounter(it.id)) } @@ -58,7 +58,7 @@ suspend fun > List.toUi( fun List.toUi( mode: ListMode, -): List = when (mode) { +): List = when (mode) { ListMode.LIST -> map { it.toListModel(0) } ListMode.DETAILED_LIST -> map { it.toListDetailedModel(0) } ListMode.GRID -> map { it.toGridModel(0) } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index b57158069..e11527071 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -45,7 +45,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.SearchActivity -import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity +import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel @@ -268,7 +268,7 @@ class MainActivity : if (source != null) { startActivity(SearchActivity.newIntent(this, source, query)) } else { - startActivity(GlobalSearchActivity.newIntent(this, query)) + startActivity(MultiSearchActivity.newIntent(this, query)) } searchSuggestionViewModel.saveQuery(query) } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt index 1d1fb43fc..b06e06bfd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt @@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.SearchViewModel -import org.koitharu.kotatsu.search.ui.global.GlobalSearchViewModel +import org.koitharu.kotatsu.search.ui.multi.MultiSearchViewModel import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel val searchModule @@ -16,11 +16,7 @@ val searchModule factory { MangaSearchRepository(get(), get(), androidContext(), get()) } factory { MangaSuggestionsProvider.createSuggestions(androidContext()) } - viewModel { params -> - SearchViewModel(MangaRepository(params[0]), params[1], get()) - } - viewModel { query -> - GlobalSearchViewModel(query.get(), get(), get()) - } + viewModel { params -> SearchViewModel(MangaRepository(params[0]), params[1], get()) } viewModel { SearchSuggestionViewModel(get(), get()) } + viewModel { params -> MultiSearchViewModel(params[0], get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index 151bb2b33..4904ab34a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -14,16 +14,16 @@ import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags -import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding +import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel -class MangaListActivity : BaseActivity() { +class MangaListActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(ActivitySearchGlobalBinding.inflate(layoutInflater)) + setContentView(ActivityContainerBinding.inflate(layoutInflater)) val tags = intent.getParcelableExtra(EXTRA_TAGS)?.tags ?: run { finishAfterTransition() return diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt deleted file mode 100644 index ad23f0b98..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.koitharu.kotatsu.search.ui.global - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.ViewGroup -import androidx.core.graphics.Insets -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding - -class GlobalSearchActivity : BaseActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivitySearchGlobalBinding.inflate(layoutInflater)) - val query = intent.getStringExtra(EXTRA_QUERY) - - if (query == null) { - finishAfterTransition() - return - } - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - title = query - supportActionBar?.subtitle = getString(R.string.search_results) - supportFragmentManager - .beginTransaction() - .replace(R.id.container, GlobalSearchFragment.newInstance(query)) - .commit() - } - - override fun onWindowInsetsChanged(insets: Insets) { - with(binding.toolbar) { - updatePadding( - left = insets.left, - right = insets.right - ) - updateLayoutParams { - topMargin = insets.top - } - } - } - - 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/search/ui/global/GlobalSearchFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt deleted file mode 100644 index 185de3d25..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.koitharu.kotatsu.search.ui.global - -import android.view.Menu -import androidx.appcompat.view.ActionMode -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.parameter.parametersOf -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.list.ui.MangaListFragment -import org.koitharu.kotatsu.utils.ext.stringArgument -import org.koitharu.kotatsu.utils.ext.withArgs - -class GlobalSearchFragment : MangaListFragment() { - - override val viewModel by viewModel { - parametersOf(query) - } - - private val query by stringArgument(ARG_QUERY) - - override fun onScrolledToEnd() = Unit - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.mode_remote, menu) - return super.onCreateActionMode(mode, menu) - } - - 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/search/ui/global/GlobalSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt deleted file mode 100644 index 3511f3b3d..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchViewModel.kt +++ /dev/null @@ -1,87 +0,0 @@ -package org.koitharu.kotatsu.search.ui.global - -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.plus -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.model.* -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.search.domain.MangaSearchRepository -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import org.koitharu.kotatsu.utils.ext.onFirst - -class GlobalSearchViewModel( - private val query: String, - private val repository: MangaSearchRepository, - settings: AppSettings -) : MangaListViewModel(settings) { - - private val mangaList = MutableStateFlow?>(null) - private val hasNextPage = MutableStateFlow(false) - private val listError = MutableStateFlow(null) - private var searchJob: Job? = null - - override val content = combine( - mangaList, - createListModeFlow(), - listError, - hasNextPage - ) { list, mode, error, hasNext -> - when { - list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) - list == null -> listOf(LoadingState) - list.isEmpty() -> listOf( - EmptyState( - icon = R.drawable.ic_book_search, - textPrimary = R.string.nothing_found, - textSecondary = R.string.text_search_holder_secondary, - actionStringRes = 0, - ) - ) - else -> { - val result = ArrayList(list.size + 1) - list.toUi(result, mode) - when { - error != null -> result += error.toErrorFooter() - hasNext -> result += LoadingFooter - } - result - } - } - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) - - init { - onRefresh() - } - - override fun onRetry() { - onRefresh() - } - - override fun onRefresh() { - searchJob?.cancel() - searchJob = repository.globalSearch(query) - .catch { e -> - listError.value = e - loadingCounter.reset() - }.onStart { - mangaList.value = null - listError.value = null - loadingCounter.increment() - hasNextPage.value = true - }.onEmpty { - mangaList.value = emptyList() - }.onCompletion { - loadingCounter.reset() - hasNextPage.value = false - }.onFirst { - loadingCounter.reset() - }.onEach { - mangaList.value = mangaList.value?.plus(it) ?: listOf(it) - }.launchIn(viewModelScope + Dispatchers.Default) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt new file mode 100644 index 000000000..5bd119a83 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt @@ -0,0 +1,183 @@ +package org.koitharu.kotatsu.search.ui.multi + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.view.ActionMode +import androidx.core.graphics.Insets +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.RecyclerView +import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding +import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.download.ui.service.DownloadService +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration +import org.koitharu.kotatsu.list.ui.adapter.MangaListListener +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.search.ui.SearchActivity +import org.koitharu.kotatsu.search.ui.multi.adapter.ItemSizeResolver +import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter +import org.koitharu.kotatsu.utils.ShareHelper +import org.koitharu.kotatsu.utils.ext.findViewsByType + +class MultiSearchActivity : BaseActivity(), MangaListListener, ActionMode.Callback { + + private val viewModel by viewModel { + parametersOf(intent.getStringExtra(EXTRA_QUERY).orEmpty()) + } + private lateinit var adapter: MultiSearchAdapter + private lateinit var selectionDecoration: MangaSelectionDecoration + private var actionMode: ActionMode? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivitySearchMultiBinding.inflate(layoutInflater)) + + val itemCLickListener = object : OnListItemClickListener { + override fun onItemClick(item: MultiSearchListModel, view: View) { + startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query.value)) + } + } + val sizeResolver = ItemSizeResolver(resources, get()) + selectionDecoration = MangaSelectionDecoration(this) + adapter = MultiSearchAdapter( + lifecycleOwner = this, + coil = get(), + listener = this, + itemClickListener = itemCLickListener, + sizeResolver = sizeResolver, + selectionDecoration = selectionDecoration, + ) + binding.recyclerView.adapter = adapter + binding.recyclerView.setHasFixedSize(true) + + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setSubtitle(R.string.search_results) + } + + viewModel.query.observe(this) { title = it } + viewModel.list.observe(this) { adapter.items = it } + } + + override fun onWindowInsetsChanged(insets: Insets) { + with(binding.toolbar) { + updatePadding( + left = insets.left, + right = insets.right, + ) + updateLayoutParams { + topMargin = insets.top + } + } + binding.recyclerView.updatePadding( + bottom = insets.bottom, + left = insets.left, + right = insets.right, + ) + } + + override fun onItemClick(item: Manga, view: View) { + if (selectionDecoration.checkedItemsCount != 0) { + selectionDecoration.toggleItemChecked(item.id) + if (selectionDecoration.checkedItemsCount == 0) { + actionMode?.finish() + } else { + actionMode?.invalidate() + invalidateItemDecorations() + } + return + } + val intent = DetailsActivity.newIntent(this, item) + startActivity(intent) + } + + override fun onItemLongClick(item: Manga, view: View): Boolean { + if (actionMode == null) { + actionMode = startSupportActionMode(this) + } + return actionMode?.also { + selectionDecoration.setItemIsChecked(item.id, true) + invalidateItemDecorations() + it.invalidate() + } != null + } + + override fun onRetryClick(error: Throwable) { + viewModel.doSearch(viewModel.query.value.orEmpty()) + } + + override fun onTagRemoveClick(tag: MangaTag) = Unit + + override fun onFilterClick() = Unit + + override fun onEmptyActionClick() = Unit + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.mode_remote, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.title = selectionDecoration.checkedItemsCount.toString() + return true + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_share -> { + ShareHelper(this).shareMangaLinks(collectSelectedItems()) + mode.finish() + true + } + R.id.action_favourite -> { + FavouriteCategoriesBottomSheet.show(supportFragmentManager, collectSelectedItems()) + mode.finish() + true + } + R.id.action_save -> { + DownloadService.confirmAndStart(this, collectSelectedItems()) + mode.finish() + true + } + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode) { + selectionDecoration.clearSelection() + invalidateItemDecorations() + actionMode = null + } + + private fun collectSelectedItems(): Set { + return viewModel.getItems(selectionDecoration.checkedItemsIds) + } + + private fun invalidateItemDecorations() { + binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach { + it.invalidateItemDecorations() + } + } + + companion object { + + private const val EXTRA_QUERY = "query" + + fun newIntent(context: Context, query: String) = + Intent(context, MultiSearchActivity::class.java) + .putExtra(EXTRA_QUERY, query) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt new file mode 100644 index 000000000..eb8d71a7e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.search.ui.multi + +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.MangaItemModel +import org.koitharu.kotatsu.parsers.model.MangaSource + +class MultiSearchListModel( + val source: MangaSource, + val list: List, +) : ListModel { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MultiSearchListModel + + if (source != other.source) return false + if (list != other.list) return false + + return true + } + + override fun hashCode(): Int { + var result = source.hashCode() + result = 31 * result + list.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt new file mode 100644 index 000000000..40b6a8619 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -0,0 +1,112 @@ +package org.koitharu.kotatsu.search.ui.multi + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug + +private const val MAX_PARALLELISM = 4 + +class MultiSearchViewModel( + initialQuery: String, + private val settings: AppSettings, +) : BaseViewModel() { + + private var searchJob: Job? = null + private val listData = MutableStateFlow>(emptyList()) + private val loadingData = MutableStateFlow(false) + private var listError = MutableStateFlow(null) + + val query = MutableLiveData(initialQuery) + val list: LiveData> = combine( + listData, + loadingData, + listError, + ) { list, loading, error -> + when { + list.isEmpty() -> listOf( + when { + loading -> LoadingState + error != null -> error.toErrorState(canRetry = true) + else -> EmptyState( + icon = R.drawable.ic_book_search, + textPrimary = R.string.nothing_found, + textSecondary = R.string.text_search_holder_secondary, + actionStringRes = 0, + ) + } + ) + loading -> list + LoadingFooter + else -> list + } + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + + init { + doSearch(initialQuery) + } + + fun getItems(ids: Set): Set { + val result = HashSet(ids.size) + listData.value.forEach { x -> + for (item in x.list) { + if (item.id in ids) { + result.add(item.manga) + } + } + } + return result + } + + fun doSearch(q: String) { + val prevJob = searchJob + searchJob = launchJob(Dispatchers.Default) { + prevJob?.cancelAndJoin() + try { + listError.value = null + listData.value = emptyList() + loadingData.value = true + query.postValue(q) + val errors = searchImpl(q) + listError.value = errors.firstOrNull() + } catch (e: Throwable) { + listError.value = e + } finally { + loadingData.value = false + } + } + } + + private suspend fun searchImpl(q: String): List { + val sources = settings.getMangaSources(includeHidden = false) + val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) + return coroutineScope { + sources.map { source -> + async(dispatcher) { + runCatching { + val list = MangaRepository(source).getList(offset = 0, query = q) + // .sortedBy { x -> x.title.levenshteinDistance(q) } + .toUi(ListMode.GRID) + if (list.isNotEmpty()) { + val item = MultiSearchListModel(source, list) + listData.update { x -> x + item } + } + }.onFailure { + it.printStackTraceDebug() + }.exceptionOrNull() + } + } + }.awaitAll().filterNotNull() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/ItemSizeResolver.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/ItemSizeResolver.kt new file mode 100644 index 000000000..a5f5d3f72 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/ItemSizeResolver.kt @@ -0,0 +1,15 @@ +package org.koitharu.kotatsu.search.ui.multi.adapter + +import android.content.res.Resources +import kotlin.math.roundToInt +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppSettings + +class ItemSizeResolver(resources: Resources, settings: AppSettings) { + + private val scaleFactor = settings.gridSize / 100f + private val gridWidth = resources.getDimension(R.dimen.preferred_grid_width) + + val cellWidth: Int + get() = (gridWidth * scaleFactor).roundToInt() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt new file mode 100644 index 000000000..35afb49d4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt @@ -0,0 +1,59 @@ +package org.koitharu.kotatsu.search.ui.multi.adapter + +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView.RecycledViewPool +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration +import org.koitharu.kotatsu.list.ui.adapter.* +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel +import kotlin.jvm.internal.Intrinsics + +class MultiSearchAdapter( + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + listener: MangaListListener, + itemClickListener: OnListItemClickListener, + sizeResolver: ItemSizeResolver, + selectionDecoration: MangaSelectionDecoration, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + val pool = RecycledViewPool() + delegatesManager + .addDelegate( + searchResultsAD( + sharedPool = pool, + lifecycleOwner = lifecycleOwner, + coil = coil, + sizeResolver = sizeResolver, + selectionDecoration = selectionDecoration, + listener = listener, + itemClickListener = itemClickListener, + ) + ) + .addDelegate(loadingStateAD()) + .addDelegate(loadingFooterAD()) + .addDelegate(emptyStateListAD(listener)) + .addDelegate(errorStateListAD(listener)) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return when { + oldItem is MultiSearchListModel && newItem is MultiSearchListModel -> { + oldItem.source == newItem.source + } + else -> oldItem.javaClass == newItem.javaClass + } + } + + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return Intrinsics.areEqual(oldItem, newItem) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt new file mode 100644 index 000000000..ee58933e8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt @@ -0,0 +1,47 @@ +package org.koitharu.kotatsu.search.ui.multi.adapter + +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.RecyclerView.RecycledViewPool +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.databinding.ItemListGroupBinding +import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration +import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel + +fun searchResultsAD( + sharedPool: RecycledViewPool, + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + sizeResolver: ItemSizeResolver, + selectionDecoration: MangaSelectionDecoration, + listener: OnListItemClickListener, + itemClickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) } +) { + + binding.recyclerView.setRecycledViewPool(sharedPool) + val adapter = ListDelegationAdapter( + mangaGridItemAD(coil, lifecycleOwner, listener, sizeResolver) + ) + binding.recyclerView.addItemDecoration(selectionDecoration) + binding.recyclerView.adapter = adapter + val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing) + binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) + val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener) + itemView.setOnClickListener(eventListener) + + bind { + binding.textViewTitle.text = item.source.title + adapter.items = item.list + adapter.notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index 021c77859..586e40eef 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext import android.app.Activity import android.graphics.Rect import android.view.View +import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import androidx.core.view.children import androidx.recyclerview.widget.LinearLayoutManager @@ -138,4 +139,19 @@ val RecyclerView.isScrolledToTop: Boolean } val holder = findViewHolderForAdapterPosition(0) return holder != null && holder.itemView.top >= 0 - } \ No newline at end of file + } + +fun ViewGroup.findViewsByType(clazz: Class): Sequence { + if (childCount == 0) { + return emptySequence() + } + return sequence { + for (view in children) { + if (clazz.isInstance(view)) { + yield(clazz.cast(view)!!) + } else if (view is ViewGroup && view.childCount != 0) { + yieldAll(view.findViewsByType(clazz)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_global.xml b/app/src/main/res/layout/activity_container.xml similarity index 100% rename from app/src/main/res/layout/activity_search_global.xml rename to app/src/main/res/layout/activity_container.xml diff --git a/app/src/main/res/layout/activity_search_multi.xml b/app/src/main/res/layout/activity_search_multi.xml new file mode 100644 index 000000000..23b613556 --- /dev/null +++ b/app/src/main/res/layout/activity_search_multi.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_list_group.xml b/app/src/main/res/layout/item_list_group.xml new file mode 100644 index 000000000..3296973f5 --- /dev/null +++ b/app/src/main/res/layout/item_list_group.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml index c0cbb05b9..5174b647a 100644 --- a/app/src/main/res/layout/item_manga_grid.xml +++ b/app/src/main/res/layout/item_manga_grid.xml @@ -27,8 +27,9 @@ android:id="@+id/textView_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:elegantTextHeight="false" android:ellipsize="end" - android:maxLines="2" + android:lines="2" android:padding="8dp" android:textAppearance="?attr/textAppearanceTitleSmall" android:textColor="?android:attr/textColorPrimary" From 17c440ee43209ad9ae57f5ec8be65c79bb9d3cca Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 17 May 2022 11:31:06 +0300 Subject: [PATCH 006/213] Fix marking sources as new --- .../koitharu/kotatsu/core/prefs/AppSettings.kt | 2 +- .../settings/sources/SourcesSettingsFragment.kt | 4 ++-- .../settings/sources/SourcesSettingsViewModel.kt | 3 +++ .../sources/adapter/SourceConfigItemDecoration.kt | 15 --------------- 4 files changed, 6 insertions(+), 18 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItemDecoration.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index b81fcbe7a..64956277b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -152,7 +152,7 @@ class AppSettings(context: Context) { } fun markKnownSources(sources: Collection) { - sourcesOrder = sourcesOrder + sources.map { it.name } + sourcesOrder = (sourcesOrder + sources.map { it.name }).distinct() } val isPagesNumbersEnabled: Boolean diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 60a5b6ff9..e89e20173 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -21,7 +21,8 @@ import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -class SourcesSettingsFragment : BaseFragment(), +class SourcesSettingsFragment : + BaseFragment(), SourceConfigListener, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener, @@ -53,7 +54,6 @@ class SourcesSettingsFragment : BaseFragment(), val sourcesAdapter = SourceConfigAdapter(this, get(), viewLifecycleOwner) with(binding.recyclerView) { setHasFixedSize(true) - // addItemDecoration(SourceConfigItemDecoration(view.context)) adapter = sourcesAdapter reorderHelper = ItemTouchHelper(SourcesReorderCallback()).also { it.attachToRecyclerView(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt index 01185ba2a..2f53c15fc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt @@ -51,6 +51,9 @@ class SourcesSettingsViewModel( } else { settings.hiddenSources + source.name } + if (isEnabled) { + settings.markKnownSources(setOf(source)) + } buildList() } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItemDecoration.kt deleted file mode 100644 index 0171b9dcf..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItemDecoration.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.koitharu.kotatsu.settings.sources.adapter - -import android.content.Context -import androidx.recyclerview.widget.RecyclerView -import org.koitharu.kotatsu.base.ui.list.decor.AbstractDividerItemDecoration - -class SourceConfigItemDecoration(context: Context) : AbstractDividerItemDecoration(context) { - - override fun shouldDrawDivider( - above: RecyclerView.ViewHolder, - below: RecyclerView.ViewHolder, - ): Boolean { - return above.itemViewType != 0 && below.itemViewType != 0 - } -} \ No newline at end of file From 3be96cf035c47e190278115ff489d36af535859a Mon Sep 17 00:00:00 2001 From: SkyfaceD Date: Tue, 17 May 2022 15:40:34 +0600 Subject: [PATCH 007/213] Hide shikimori sensitive information --- app/build.gradle | 3 +++ .../kotatsu/shikimori/data/ShikimoriRepository.kt | 9 ++++----- build.gradle | 9 +++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ddd401a15..aa3e958a6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,6 +24,9 @@ android { arg 'room.schemaLocation', "$projectDir/schemas".toString() } } + + buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', localProperty('shikimori.clientId') + buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', localProperty('shikimori.clientSecret') } buildTypes { debug { diff --git a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt index 2cfca1505..e8dfae344 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shikimori/data/ShikimoriRepository.kt @@ -4,6 +4,7 @@ import okhttp3.FormBody import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request +import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.json.mapJSON @@ -14,8 +15,6 @@ import org.koitharu.kotatsu.shikimori.data.model.ShikimoriManga import org.koitharu.kotatsu.shikimori.data.model.ShikimoriMangaInfo import org.koitharu.kotatsu.shikimori.data.model.ShikimoriUser -private const val CLIENT_ID = "Mw6F0tPEOgyV7F9U9Twg50Q8SndMY7hzIOfXg0AX_XU" -private const val CLIENT_SECRET = "euBMt1GGRSDpVIFQVPxZrO7Kh6X4gWyv0dABuj4B-M8" private const val REDIRECT_URI = "kotatsu://shikimori-auth" private const val BASE_URL = "https://shikimori.one/api/" private const val MANGA_PAGE_SIZE = 10 @@ -26,7 +25,7 @@ class ShikimoriRepository( ) { val oauthUrl: String - get() = "https://shikimori.one/oauth/authorize?client_id=$CLIENT_ID&" + + get() = "https://shikimori.one/oauth/authorize?client_id=${BuildConfig.SHIKIMORI_CLIENT_ID}&" + "redirect_uri=$REDIRECT_URI&response_type=code&scope=" val isAuthorized: Boolean @@ -35,8 +34,8 @@ class ShikimoriRepository( suspend fun authorize(code: String?) { val body = FormBody.Builder() body.add("grant_type", "authorization_code") - body.add("client_id", CLIENT_ID) - body.add("client_secret", CLIENT_SECRET) + body.add("client_id", BuildConfig.SHIKIMORI_CLIENT_ID) + body.add("client_secret", BuildConfig.SHIKIMORI_CLIENT_SECRET) if (code != null) { body.add("redirect_uri", REDIRECT_URI) body.add("code", code) diff --git a/build.gradle b/build.gradle index 390d5748a..be41a3021 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,15 @@ allprojects { } } +Object localProperty(String name, Object defaultValue = 'null') { + Properties localProperties = new Properties() + project.rootProject.file('local.properties').withInputStream { localProperties.load(it) } + + def value = localProperties[name] + + return value != null ? value : defaultValue +} + task clean(type: Delete) { delete rootProject.buildDir } \ No newline at end of file From ce7960e5e9f7099a618ad2e96fd6feaa26223334 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 17 May 2022 13:23:03 +0300 Subject: [PATCH 008/213] Recreate all activities on theme changed --- .../java/org/koitharu/kotatsu/KotatsuApp.kt | 2 ++ .../base/ui/util/ActivityRecreationHandle.kt | 34 +++++++++++++++++++ .../org/koitharu/kotatsu/main/MainModule.kt | 2 ++ .../settings/AppearanceSettingsFragment.kt | 8 +++-- app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 19 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 4ae7a438f..e00dd1680 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.strictmode.FragmentStrictMode import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.bookmarks.bookmarksModule import org.koitharu.kotatsu.core.db.databaseModule import org.koitharu.kotatsu.core.github.githubModule @@ -43,6 +44,7 @@ class KotatsuApp : Application() { Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext)) AppCompatDelegate.setDefaultNightMode(get().theme) registerActivityLifecycleCallbacks(get()) + registerActivityLifecycleCallbacks(get()) val widgetUpdater = WidgetUpdater(applicationContext) widgetUpdater.subscribeToFavourites(get()) widgetUpdater.subscribeToHistory(get()) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt new file mode 100644 index 000000000..f072da2fa --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt @@ -0,0 +1,34 @@ +package org.koitharu.kotatsu.base.ui.util + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import java.util.* + +class ActivityRecreationHandle : ActivityLifecycleCallbacks { + + private val activities = WeakHashMap() + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + activities[activity] = Unit + } + + override fun onActivityStarted(activity: Activity) = Unit + + override fun onActivityResumed(activity: Activity) = Unit + + override fun onActivityPaused(activity: Activity) = Unit + + override fun onActivityStopped(activity: Activity) = Unit + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit + + override fun onActivityDestroyed(activity: Activity) { + activities.remove(activity) + } + + fun recreateAll() { + val snapshot = activities.keys.toList() + snapshot.forEach { it.recreate() } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt index c6e11107b..7bcca45d6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.main import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module +import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.main.ui.MainViewModel import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper @@ -11,6 +12,7 @@ import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel val mainModule get() = module { single { AppProtectHelper(get()) } + single { ActivityRecreationHandle() } factory { ShortcutsRepository(androidContext(), get(), get(), get()) } viewModel { MainViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt index 81817e8b5..11337c1b6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt @@ -8,15 +8,17 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.TwoStatePreference +import java.util.* +import org.koin.android.ext.android.get import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat -import java.util.* class AppearanceSettingsFragment : BasePreferenceFragment(R.string.appearance), @@ -71,10 +73,10 @@ class AppearanceSettingsFragment : AppCompatDelegate.setDefaultNightMode(settings.theme) } AppSettings.KEY_DYNAMIC_THEME -> { - findPreference(key)?.setSummary(R.string.restart_required) + get().recreateAll() } AppSettings.KEY_THEME_AMOLED -> { - findPreference(key)?.setSummary(R.string.restart_required) + get().recreateAll() } AppSettings.KEY_APP_PASSWORD -> { findPreference(AppSettings.KEY_PROTECT_APP) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 184acc744..c91af2b09 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -162,7 +162,6 @@ Зыходны памер Чорная цёмная тэма Карысна для AMOLED экранаў - Патрэбны перазапуск Рэзервовае капіяванне і аднаўленне Стварыць рэзервовую копію Аднавіць данныя diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 681a9ce9a..05d8c57b1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -85,7 +85,6 @@ An Höhe anpassen Nützlich für AMOLED-Bildschirme Schwarzer dunkler Modus - Neustart erforderlich Von rechts nach links Neue Kategorie Sicherung und Wiederherstellung diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 78dc8e948..e43010431 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -162,7 +162,6 @@ Mantener al iniciar Tema oscuro auténtico Útil para pantallas AMOLED - Se requiere reinicio Respaldo y restauración Crear copia de seguridad de datos Restaurar desde la copia de seguridad diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 51e4c397f..6f27fcae2 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -53,7 +53,6 @@ Palauta varmuuskopiosta Luo tietojen varmuuskopio Varmuuskopiointi ja palautus - Uudelleenkäynnistys vaaditaan Hyödyllinen AMOLED-näytöille Musta tumma teema Pidä alussa diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ea0879080..12629d2cb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -42,7 +42,6 @@ Restaurer à partir d\'une sauvegarde Créer une sauvegarde des données Sauvegarde et restauration - Redémarrage nécessaire Utilise moins d\'énergie pour les écrans AMOLED Noir Garder au début diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 166a651d4..1a72f7f60 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -91,7 +91,6 @@ Ripristina da un backup Crea un backup dei dati Backup e ripristino - Riavvio richiesto Utile per gli schermi AMOLED Tema nero scuro Segnala un problema su GitHub diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 141909e39..4865c94eb 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -221,7 +221,6 @@ NSFW漫画を履歴から除外する キュー 全てのCookieが削除されました - 再起動が必要です 一部のデバイスはシステムでの動作が異なり、バックグラウンドタスクが中断される可能性があります。 ジャンル スケールモード diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 01af34997..5857f9580 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -102,7 +102,6 @@ Data gjenopprettet Gjenopprett fra sikkerhetskopi Opprett sikkerhetskopi - Omstart kreves Bruker mindre strøm på AMOLED-skjermer Svart Ny kategori diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b9c94ef0c..d94f70c75 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -240,7 +240,6 @@ Criar backup de dados Salve-o de fontes online ou importe arquivos. Verifique se há atualizações - É necessário reiniciar Novos capítulos do que você está lendo são mostrados aqui Versão %s Ajustar à largura diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 6f2d53e66..0b92d414f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -139,7 +139,6 @@ Modo de escala Centro de ajuste Ajustar à largura - É necessário reiniciar Backup e restauração Criar backup de dados Restaurar do backup diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cd2f2e8a4..505aba081 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -167,7 +167,6 @@ Исходный размер Чёрная Потребляет меньше энергии на экранах AMOLED - Требуется перезапуск Резервное копирование и восстановление Создать резервную копию Восстановить данные diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 582683f3a..ca336dc58 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -247,7 +247,6 @@ Anpassa mot bredd Svart Använder mindre ström på AMOLED-skärmar - Omstart krävs Säkerhetskopiering och återställning Skapa säkerhetskopia Återställd diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b0fe27dd5..458ff2d7b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -202,7 +202,6 @@ Uzun zaman önce Bugün Güncelleme yok - Yeniden başlatma gerekli Yeni bölümler denetleniyor: %1$d / %2$d Dinamik tema Favorilerinizi düzenlemek için kategorileri kullanabilirsiniz. Kategori oluşturmak için «+» düğmesine basın diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cea12d99c..9fd8db95a 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -143,7 +143,6 @@ Вихідний розмір Чорна Споживає менше енергії на екранах AMOLED - Потрібен перезапуск Резервне копіювання та відновлення Відновлено Підготовка… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c3749375..254f66818 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -168,7 +168,6 @@ Keep at start Black Uses less power on AMOLED screens - Restart required Backup and restore Create data backup Restore from backup From 473135bfc53025958bb1e9db2a00573bc9c42e27 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 17 May 2022 14:10:40 +0300 Subject: [PATCH 009/213] Apply theme changing without restarting --- .../org/koitharu/kotatsu/base/ui/BaseActivity.kt | 8 ++++++-- .../koitharu/kotatsu/core/prefs/AppSettings.kt | 10 +--------- .../settings/AppearanceSettingsFragment.kt | 16 ++++++++++++---- .../kotatsu/settings/SettingsHeadersFragment.kt | 2 +- app/src/main/res/values-night/themes.xml | 9 ++++++--- app/src/main/res/values/themes.xml | 6 ++++-- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index 2fcfeac76..9cdce9654 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -43,9 +43,13 @@ abstract class BaseActivity : override fun onCreate(savedInstanceState: Bundle?) { val settings = get() + val isAmoled = settings.isAmoledTheme + val isDynamic = settings.isDynamicTheme + // TODO support DialogWhenLarge theme when { - settings.isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED) - settings.isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet) + isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled) + isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled) + isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet) } super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 64956277b..ffa294262 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager import android.net.Uri -import android.os.Build import android.provider.Settings import androidx.appcompat.app.AppCompatDelegate import androidx.collection.arraySetOf @@ -52,7 +51,7 @@ class AppSettings(context: Context) { get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM val isDynamicTheme: Boolean - get() = prefs.getBoolean(KEY_DYNAMIC_THEME, false) + get() = DynamicColors.isDynamicColorAvailable() && prefs.getBoolean(KEY_DYNAMIC_THEME, false) val isAmoledTheme: Boolean get() = prefs.getBoolean(KEY_THEME_AMOLED, false) @@ -324,12 +323,5 @@ class AppSettings(context: Context) { private const val NETWORK_NEVER = 0 private const val NETWORK_ALWAYS = 1 private const val NETWORK_NON_METERED = 2 - - val isDynamicColorAvailable: Boolean - get() = DynamicColors.isDynamicColorAvailable() || - (isSamsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - - private val isSamsung - get() = Build.MANUFACTURER.equals("samsung", ignoreCase = true) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt index 11337c1b6..50bb254df 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt @@ -5,10 +5,11 @@ import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatDelegate +import androidx.core.view.postDelayed import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.TwoStatePreference -import java.util.* +import com.google.android.material.color.DynamicColors import org.koin.android.ext.android.get import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment @@ -19,6 +20,7 @@ import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat +import java.util.* class AppearanceSettingsFragment : BasePreferenceFragment(R.string.appearance), @@ -38,7 +40,7 @@ class AppearanceSettingsFragment : entryValues = ListMode.values().names() setDefaultValueCompat(ListMode.GRID.name) } - findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = AppSettings.isDynamicColorAvailable + findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = DynamicColors.isDynamicColorAvailable() findPreference(AppSettings.KEY_DATE_FORMAT)?.run { entryValues = resources.getStringArray(R.array.date_formats) val now = Date().time @@ -73,10 +75,10 @@ class AppearanceSettingsFragment : AppCompatDelegate.setDefaultNightMode(settings.theme) } AppSettings.KEY_DYNAMIC_THEME -> { - get().recreateAll() + postRestart() } AppSettings.KEY_THEME_AMOLED -> { - get().recreateAll() + postRestart() } AppSettings.KEY_APP_PASSWORD -> { findPreference(AppSettings.KEY_PROTECT_APP) @@ -100,4 +102,10 @@ class AppearanceSettingsFragment : else -> super.onPreferenceTreeClick(preference) } } + + private fun postRestart() { + view?.postDelayed(400) { + get().recreateAll() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt index bec03f017..052aba070 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt @@ -33,7 +33,7 @@ class SettingsHeadersFragment : PreferenceHeaderFragmentCompat(), SlidingPaneLay fun setTitle(title: CharSequence?) { currentTitle = title - if (slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) { + if (slidingPaneLayout.isOpen) { activity?.title = title } } diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 728707021..8696139cf 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -3,9 +3,12 @@ + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f6addbe8f..69f2615b7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -86,9 +86,11 @@ + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 69f2615b7..18c2b0c84 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -35,6 +35,8 @@ @color/errorContainer @color/onErrorContainer + @color/divider_default + ?attr/colorSurfaceVariant @@ -81,8 +83,6 @@ - \ No newline at end of file From 790f1fb8a311d5e1fd6dc9a90ed9a4ea35d39014 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 20 May 2022 12:06:26 +0300 Subject: [PATCH 014/213] Update parsers --- app/build.gradle | 4 ++-- .../org/koitharu/kotatsu/core/db/entity/EntityMapping.kt | 2 +- .../java/org/koitharu/kotatsu/local/data/PagesCache.kt | 6 +++--- .../kotatsu/local/domain/LocalMangaRepository.kt | 2 +- .../org/koitharu/kotatsu/reader/data/ModelMapping.kt | 4 ++-- .../java/org/koitharu/kotatsu/utils/ext/StringExt.kt | 9 +++++++++ 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e81717951..48da5314c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,7 +66,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:05a93e2380') { + implementation('com.github.nv95:kotatsu-parsers:f46c5add46') { exclude group: 'org.json', module: 'json' } @@ -103,7 +103,7 @@ dependencies { implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'io.insert-koin:koin-android:3.2.0' - implementation 'io.coil-kt:coil-base:2.0.0' + implementation 'io.coil-kt:coil-base:2.1.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt index 51cd77b2e..5bdd0ca4a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt @@ -3,9 +3,9 @@ package org.koitharu.kotatsu.core.db.entity import java.util.* import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase +import org.koitharu.kotatsu.utils.ext.longHashCode // Entity to model diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt index 73dd83bb4..7aee467a5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt @@ -2,13 +2,13 @@ package org.koitharu.kotatsu.local.data import android.content.Context import com.tomclaw.cache.DiskLruCache -import java.io.File -import java.io.InputStream import kotlinx.coroutines.flow.MutableStateFlow -import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.utils.FileSize +import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.subdir import org.koitharu.kotatsu.utils.ext.takeIfReadable +import java.io.File +import java.io.InputStream class PagesCache(context: Context) { diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index e034d0672..2c0b37298 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -15,11 +15,11 @@ import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.TempFileFilter import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.parsers.util.toCamelCase import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.CompositeMutex import org.koitharu.kotatsu.utils.ext.deleteAwait +import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.resolveName import java.io.File diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt index 289f44386..2cd9ffcb3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt @@ -5,10 +5,10 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter fun Manga.filterChapters(branch: String?): Manga { if (chapters.isNullOrEmpty()) return this - return copy(chapters = chapters?.filter { it.branch == branch }) + return withChapters(chapters = chapters?.filter { it.branch == branch }) } -private fun Manga.copy(chapters: List?) = Manga( +private fun Manga.withChapters(chapters: List?) = Manga( id = id, title = title, altTitle = altTitle, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index 48ba02122..badf5ae7c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -2,4 +2,13 @@ package org.koitharu.kotatsu.utils.ext inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String { return if (this.isNullOrEmpty()) defaultValue() else this +} + +fun String.longHashCode(): Long { + var h = 1125899906842597L + val len: Int = this.length + for (i in 0 until len) { + h = 31 * h + this[i].code + } + return h } \ No newline at end of file From 58c9f75b91c80c8a44189732340cb9a1b2136a5b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 20 May 2022 12:14:14 +0300 Subject: [PATCH 015/213] Fix tags order in filter --- .../org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt index 1fbf4b70e..5ea361168 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.list.ui.filter import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers @@ -28,7 +29,7 @@ class FilterCoordinator( } private var availableTagsDeferred = loadTagsAsync() - val items = getItemsFlow() + val items: LiveData> = getItemsFlow() .asLiveDataDistinct(coroutineScope.coroutineContext + Dispatchers.Default) init { @@ -105,7 +106,7 @@ class FilterCoordinator( query: String, ): List { val sortOrders = repository.sortOrders.sortedBy { it.ordinal } - val tags = mergeTags(state.tags, allTags.tags).sortedBy { it.title } + val tags = mergeTags(state.tags, allTags.tags).toList() val list = ArrayList(tags.size + sortOrders.size + 3) if (query.isEmpty()) { if (sortOrders.isNotEmpty()) { From 2b61b27271435b50a11b109bf1891a7931eb778c Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 17 May 2022 15:28:41 +0200 Subject: [PATCH 016/213] Translated using Weblate (Ukrainian) Currently translated at 100.0% (297 of 297 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Co-authored-by: Artem Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/ Translation: Kotatsu/Strings Translation: Kotatsu/plurals --- app/src/main/res/values-uk/plurals.xml | 2 +- app/src/main/res/values-uk/strings.xml | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-uk/plurals.xml b/app/src/main/res/values-uk/plurals.xml index 0ced943b7..1b47f743e 100644 --- a/app/src/main/res/values-uk/plurals.xml +++ b/app/src/main/res/values-uk/plurals.xml @@ -2,7 +2,7 @@ %1$d новий розділ - %1$d нових розділи + %1$d нові розділи %1$d нових розділів %1$d нових розділів diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9fd8db95a..b26c58fd1 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -64,7 +64,7 @@ Кеш Б|кБ|МБ|ГБ|ТБ Стандартний - Манхва + Вебтун Режим читання Розмір сітки Пошук по %s @@ -290,4 +290,8 @@ Додано закладку Скасувати Видалено з історії + DNS через HTTPS + Типовий режим + Автоматично визначати, чи є манга вебтуном + Автовизначення режиму читання \ No newline at end of file From 312fb033e05e0e56261c3f4e4f074b3a074f922b Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 20 May 2022 18:40:53 +0300 Subject: [PATCH 017/213] Fix weird toolbar in category edit activity --- .../ui/categories/edit/FavouritesCategoryEditActivity.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt index 27239c0dc..b38dfec26 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt @@ -6,10 +6,12 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.core.graphics.Insets import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -84,9 +86,9 @@ class FavouritesCategoryEditActivity : BaseActivity right = insets.right, bottom = insets.bottom, ) - binding.toolbar.updatePadding( - top = insets.top, - ) + binding.toolbar.updateLayoutParams { + topMargin = insets.top + } } override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { From fc2820ec11209bb5e4dc3a46de7eb1afa1d46293 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 20 May 2022 19:12:58 +0300 Subject: [PATCH 018/213] Update version to v3.3 --- .gitignore | 1 + app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3ba4daee9..418215154 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /.idea/dictionaries /.idea/modules.xml /.idea/misc.xml +/.idea/discord.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml diff --git a/app/build.gradle b/app/build.gradle index 48da5314c..e1c007038 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 408 - versionName '3.3-beta1' + versionCode 409 + versionName '3.3' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 0c07e649bfc32a50de8861169a21167f8e443ba8 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 21 May 2022 01:32:27 +0300 Subject: [PATCH 019/213] [Issue template] Update version --- .github/ISSUE_TEMPLATE/report_issue.yml | 4 ++-- .github/ISSUE_TEMPLATE/request_feature.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index c640ff3c0..e0a417b3f 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -44,7 +44,7 @@ body: label: Kotatsu version description: You can find your Kotatsu version in **Settings → About**. placeholder: | - Example: "3.2.3" + Example: "3.3" validations: required: true @@ -87,7 +87,7 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.3](https://github.com/nv95/Kotatsu/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index bae1b501c..d4b373203 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.3](https://github.com/nv95/Kotatsu/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file From 4f3fef3bfed9b47515a5ed7265b6facbbfcfb2be Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 25 May 2022 10:04:06 +0300 Subject: [PATCH 020/213] Update parsers --- app/build.gradle | 9 ++++- .../kotatsu/core/parser/DummyParser.kt | 4 ++- .../kotatsu/core/model/MangaSource.kt | 10 ++++++ .../kotatsu/core/parser/MangaRepository.kt | 13 +++---- .../core/parser/RemoteMangaRepository.kt | 13 +++---- .../local/domain/LocalMangaRepository.kt | 35 ++++++++++-------- .../kotatsu/local/ui/LocalListViewModel.kt | 4 +-- .../search/domain/MangaSearchRepository.kt | 4 +-- .../newsources/NewSourcesViewModel.kt | 3 +- .../sources/SourcesSettingsViewModel.kt | 7 ++-- .../adapter/SourceConfigAdapterDelegates.kt | 15 ++++---- .../main/res/layout/item_source_config.xml | 36 ++++++++++++++----- app/src/main/res/layout/navigation_header.xml | 4 ++- 13 files changed, 103 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt diff --git a/app/build.gradle b/app/build.gradle index e1c007038..108f28f92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,9 +64,16 @@ android { unitTests.returnDefaultValues = false } } +afterEvaluate { + compileDebugKotlin { + kotlinOptions { + freeCompilerArgs += ['-opt-in=kotlin.RequiresOptIn'] + } + } +} dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:f46c5add46') { + implementation('com.github.nv95:kotatsu-parsers:ab87a50e9b') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt index 4323e3a5f..479ada3ec 100644 --- a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt +++ b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.parser import java.util.* +import org.koitharu.kotatsu.parsers.InternalParsersApi import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.config.ConfigKey @@ -9,6 +10,7 @@ import org.koitharu.kotatsu.parsers.model.* /** * This parser is just for parser development, it should not be used in releases */ +@OptIn(InternalParsersApi::class) class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) { override val configKeyDomain: ConfigKey.Domain @@ -25,7 +27,7 @@ class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaS offset: Int, query: String?, tags: Set?, - sortOrder: SortOrder? + sortOrder: SortOrder, ): List { TODO("Not yet implemented") } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt new file mode 100644 index 000000000..9bd4ef5cf --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.core.model + +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.toTitleCase +import java.util.* + +fun MangaSource.getLocaleTitle(): String? { + val lc = Locale(locale ?: return null) + return lc.getDisplayLanguage(lc).toTitleCase(lc) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index ca4cc495b..812fc9e81 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -1,11 +1,11 @@ package org.koitharu.kotatsu.core.parser -import java.lang.ref.WeakReference -import java.util.* import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.* +import java.lang.ref.WeakReference +import java.util.* interface MangaRepository { @@ -13,12 +13,9 @@ interface MangaRepository { val sortOrders: Set - suspend fun getList( - offset: Int, - query: String? = null, - tags: Set? = null, - sortOrder: SortOrder? = null, - ): List + suspend fun getList(offset: Int, query: String?): List + + suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List suspend fun getDetails(manga: Manga): Manga diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 14a113e24..358bd2645 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -20,12 +20,13 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository { getConfig().defaultSortOrder = value } - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder?, - ): List = parser.getList(offset, query, tags, sortOrder) + override suspend fun getList(offset: Int, query: String?): List { + return parser.getList(offset, query) + } + + override suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { + return parser.getList(offset, tags, sortOrder) + } override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 2c0b37298..6eef24c86 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -37,28 +37,25 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma private val filenameFilter = CbzFilter() private val locks = CompositeMutex() - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder? - ): List { + override suspend fun getList(offset: Int, query: String?): List { if (offset > 0) { return emptyList() } - val files = getAllFiles() - val list = coroutineScope { - val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM) - files.map { file -> - getFromFileAsync(file, dispatcher) - }.awaitAll() - }.filterNotNullTo(ArrayList(files.size)) + val list = getRawList() if (!query.isNullOrEmpty()) { list.retainAll { x -> x.title.contains(query, ignoreCase = true) || x.altTitle?.contains(query, ignoreCase = true) == true } } + return list + } + + override suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { + if (offset > 0) { + return emptyList() + } + val list = getRawList() if (!tags.isNullOrEmpty()) { list.retainAll { x -> x.tags.containsAll(tags) @@ -244,7 +241,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma } } - override val sortOrders = emptySet() + override val sortOrders = setOf(SortOrder.ALPHABETICAL) override suspend fun getPageUrl(page: MangaPage) = page.url @@ -295,6 +292,16 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma locks.unlock(id) } + private suspend fun getRawList(): ArrayList { + val files = getAllFiles() + return coroutineScope { + val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM) + files.map { file -> + getFromFileAsync(file, dispatcher) + }.awaitAll() + }.filterNotNullTo(ArrayList(files.size)) + } + private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir -> dir.listFiles(filenameFilter)?.toList().orEmpty() } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 7ca7e8249..375e03997 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.local.ui import android.net.Uri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -24,6 +23,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.Progress +import java.io.IOException class LocalListViewModel( private val repository: LocalMangaRepository, @@ -115,7 +115,7 @@ class LocalListViewModel( private suspend fun doRefresh() { try { listError.value = null - mangaList.value = repository.getList(0) + mangaList.value = repository.getList(0, null, null) } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 270238553..2c95fb632 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -17,7 +17,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider @@ -35,7 +34,6 @@ class MangaSearchRepository( MangaRepository(source).getList( offset = 0, query = query, - sortOrder = SortOrder.POPULARITY ) }.getOrElse { emptyList() @@ -141,4 +139,4 @@ class MangaSearchRepository( return false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt index 530851d46..08ef96c49 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.newsources import androidx.lifecycle.MutableLiveData import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem @@ -33,7 +34,7 @@ class NewSourcesViewModel( sources.value = initialList.map { SourceConfigItem.SourceItem( source = it, - summary = null, + summary = it.getLocaleTitle(), isEnabled = it.name !in hidden, isDraggable = false, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt index 2f53c15fc..da3eba14f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt @@ -4,6 +4,7 @@ import androidx.core.os.LocaleListCompat import androidx.lifecycle.MutableLiveData import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.toTitleCase @@ -82,7 +83,7 @@ class SourcesSettingsViewModel( } SourceConfigItem.SourceItem( source = it, - summary = null, + summary = it.getLocaleTitle(), isEnabled = it.name !in hiddenSources, isDraggable = false, ) @@ -105,7 +106,7 @@ class SourcesSettingsViewModel( enabledSources.mapTo(result) { SourceConfigItem.SourceItem( source = it, - summary = getLocaleTitle(it.locale), + summary = it.getLocaleTitle(), isEnabled = true, isDraggable = true, ) @@ -162,4 +163,4 @@ class SourcesSettingsViewModel( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index 752e3d33e..775da0f6e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -17,15 +17,17 @@ import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.textAndVisible -fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } -) { +fun sourceConfigHeaderDelegate() = + adapterDelegateViewBinding( + { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } + ) { - bind { - binding.textViewTitle.setText(item.titleResId) + bind { + binding.textViewTitle.setText(item.titleResId) + } } -} fun sourceConfigGroupDelegate( listener: SourceConfigListener, @@ -61,6 +63,7 @@ fun sourceConfigItemDelegate( bind { binding.textViewTitle.text = item.source.title binding.switchToggle.isChecked = item.isEnabled + binding.textViewDescription.textAndVisible = item.summary imageRequest = ImageRequest.Builder(context) .data(item.faviconUrl) .error(R.drawable.ic_favicon_fallback) diff --git a/app/src/main/res/layout/item_source_config.xml b/app/src/main/res/layout/item_source_config.xml index a0620433a..859105c96 100644 --- a/app/src/main/res/layout/item_source_config.xml +++ b/app/src/main/res/layout/item_source_config.xml @@ -3,8 +3,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="?android:listPreferredItemHeightSmall" + android:layout_height="wrap_content" android:gravity="center_vertical" + android:minHeight="?android:listPreferredItemHeightSmall" android:orientation="horizontal"> - + android:orientation="vertical"> + + + + + + + android:src="@drawable/ic_totoro" + app:tint="?colorPrimary" /> Date: Mon, 30 May 2022 13:05:26 +0300 Subject: [PATCH 021/213] Update dependencies --- app/build.gradle | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 108f28f92..8f80d9a29 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,26 +77,24 @@ dependencies { exclude group: 'org.json', module: 'json' } - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2' - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.activity:activity-ktx:1.4.0' - implementation 'androidx.fragment:fragment-ktx:1.4.1' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-service:2.4.1' - implementation 'androidx.lifecycle:lifecycle-process:2.4.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.core:core-ktx:1.8.0-rc02' + implementation 'androidx.activity:activity-ktx:1.5.0-rc01' + implementation 'androidx.fragment:fragment-ktx:1.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-service:2.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-process:2.5.0-rc01' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' - implementation 'com.google.android.material:material:1.7.0-alpha01' + implementation 'com.google.android.material:material:1.7.0-alpha02' //noinspection LifecycleAnnotationProcessorWithJava8 - kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' + kapt 'androidx.lifecycle:lifecycle-compiler:2.5.0-rc01' implementation 'androidx.room:room-runtime:2.4.2' implementation 'androidx.room:room-ktx:2.4.2' @@ -117,7 +115,7 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' testImplementation 'io.insert-koin:koin-test-junit4:3.2.0' androidTestImplementation 'androidx.test:runner:1.4.0' From a74b623c1084b4369d8063fbea121e0b8ce30746 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 30 May 2022 15:45:29 +0300 Subject: [PATCH 022/213] Refactor menu providers --- .../kotatsu/base/ui/BaseBottomSheet.kt | 2 +- .../base/ui/util/WindowInsetsDelegate.kt | 7 +-- .../kotatsu/details/ui/ChaptersFragment.kt | 59 +++++++++-------- .../kotatsu/details/ui/DetailsActivity.kt | 13 ---- .../kotatsu/details/ui/DetailsFragment.kt | 36 ++++++++--- .../ui/FavouritesContainerFragment.kt | 27 ++------ .../ui/FavouritesContainerMenuProvider.kt | 28 +++++++++ .../ui/list/FavouritesListFragment.kt | 46 ++------------ .../ui/list/FavouritesListMenuProvider.kt | 48 ++++++++++++++ .../kotatsu/history/ui/HistoryListFragment.kt | 35 +---------- .../history/ui/HistoryListMenuProvider.kt | 41 ++++++++++++ .../kotatsu/list/ui/MangaListFragment.kt | 20 +----- .../kotatsu/list/ui/MangaListMenuProvider.kt | 25 ++++++++ .../kotatsu/local/ui/LocalListFragment.kt | 18 +----- .../kotatsu/local/ui/LocalListMenuProvider.kt | 26 ++++++++ .../kotatsu/reader/ui/PageSaveContract.kt | 2 +- .../remotelist/ui/RemoteListFragment.kt | 49 ++++++++------- .../settings/backup/BackupDialogFragment.kt | 17 ++--- .../sources/SourcesSettingsFragment.kt | 63 ++++++++++--------- .../suggestions/ui/SuggestionsFragment.kt | 35 ++++++----- .../kotatsu/tracker/ui/FeedFragment.kt | 42 ++----------- .../kotatsu/tracker/ui/FeedMenuProvider.kt | 48 ++++++++++++++ .../koitharu/kotatsu/utils/ext/FragmentExt.kt | 6 ++ .../koitharu/kotatsu/utils/ext/LocaleExt.kt | 8 ++- 24 files changed, 396 insertions(+), 305 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt index 7378b5acf..23ec29339 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt @@ -45,7 +45,7 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { requireContext().displayCompat?.let { val metrics = DisplayMetrics() it.getRealMetrics(metrics) - behavior?.peekHeight = metrics.heightPixels / 2 + behavior?.peekHeight = (metrics.heightPixels * 0.4).toInt() } return binding.root diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt index 5f1f0cd5e..c0d9c8c53 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt @@ -16,10 +16,7 @@ class WindowInsetsDelegate( private var lastInsets: Insets? = null - override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat?): WindowInsetsCompat? { - if (insets == null) { - return null - } + override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { val handledInsets = interceptingWindowInsetsListener?.onApplyWindowInsets(v, insets) ?: insets val newInsets = if (handleImeInsets) { Insets.max( @@ -49,7 +46,7 @@ class WindowInsetsDelegate( ) { view.removeOnLayoutChangeListener(this) if (lastInsets == null) { // Listener may not be called - onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view)) + onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view) ?: return) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index a9a57e30e..283a141d3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.core.view.updatePadding import com.google.android.material.snackbar.Snackbar @@ -27,6 +28,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback +import org.koitharu.kotatsu.utils.ext.addMenuProvider import kotlin.math.roundToInt class ChaptersFragment : @@ -43,11 +45,6 @@ class ChaptersFragment : private var actionMode: ActionMode? = null private var selectionDecoration: ChaptersSelectionDecoration? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -72,6 +69,7 @@ class ChaptersFragment : binding.textViewHolder.isVisible = it activity?.invalidateOptionsMenu() } + addMenuProvider(ChaptersMenuProvider()) } override fun onDestroyView() { @@ -81,31 +79,6 @@ class ChaptersFragment : super.onDestroyView() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_chapters, menu) - val searchMenuItem = menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true - menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_reversed -> { - viewModel.setChaptersReversed(!item.isChecked) - true - } - else -> super.onOptionsItemSelected(item) - } - override fun onItemClick(item: ChapterListItem, view: View) { if (selectionDecoration?.checkedItemsCount != 0) { selectionDecoration?.toggleItemChecked(item.chapter.id) @@ -268,4 +241,30 @@ class ChaptersFragment : private fun onLoadingStateChanged(isLoading: Boolean) { binding.progressBar.isVisible = isLoading } + + private inner class ChaptersMenuProvider : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_chapters, menu) + val searchMenuItem = menu.findItem(R.id.action_search) + searchMenuItem.setOnActionExpandListener(this@ChaptersFragment) + val searchView = searchMenuItem.actionView as SearchView + searchView.setOnQueryTextListener(this@ChaptersFragment) + searchView.setIconifiedByDefault(false) + searchView.queryHint = searchMenuItem.title + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true + menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_reversed -> { + viewModel.setChaptersReversed(!menuItem.isChecked) + true + } + else -> false + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index ec4b8e499..dc5e6a513 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -15,8 +15,6 @@ import android.widget.Toast import androidx.appcompat.view.ActionMode import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.Insets -import androidx.core.net.toFile -import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding @@ -45,7 +43,6 @@ import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity -import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.getDisplayMessage class DetailsActivity : @@ -166,16 +163,6 @@ class DetailsActivity : } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_share -> { - viewModel.manga.value?.let { - if (it.source == MangaSource.LOCAL) { - ShareHelper(this).shareCbz(listOf(it.url.toUri().toFile())) - } else { - ShareHelper(this).shareMangaLink(it) - } - } - true - } R.id.action_delete -> { val title = viewModel.manga.value?.title.orEmpty() MaterialAlertDialogBuilder(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 54ae95a5b..dfc427d99 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -8,8 +8,10 @@ import android.view.* import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.core.graphics.Insets +import androidx.core.net.toFile import androidx.core.net.toUri import androidx.core.text.parseAsHtml +import androidx.core.view.MenuProvider import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding @@ -40,6 +42,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.utils.FileSize +import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.* class DetailsFragment : @@ -52,11 +55,6 @@ class DetailsFragment : private val viewModel by sharedViewModel() private val coil by inject(mode = LazyThreadSafetyMode.NONE) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup?, @@ -76,11 +74,7 @@ class DetailsFragment : viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged) viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged) viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_details_info, menu) + addMenuProvider(DetailsMenuProvider()) } override fun onItemClick(item: Bookmark, view: View) { @@ -329,4 +323,26 @@ class DetailsFragment : } ?: request.fallback(R.drawable.ic_placeholder) request.enqueueWith(coil) } + + private inner class DetailsMenuProvider : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_details_info, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_share -> { + viewModel.manga.value?.let { + val context = requireContext() + if (it.source == MangaSource.LOCAL) { + ShareHelper(context).shareCbz(listOf(it.url.toUri().toFile())) + } else { + ShareHelper(context).shareMangaLink(it) + } + } + true + } + else -> false + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 8e9d945a0..22939d9d9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.favourites.ui import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.Insets @@ -19,12 +21,12 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeListener import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding -import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.main.ui.AppBarOwner +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.resolveDp @@ -43,11 +45,6 @@ class FavouritesContainerFragment : private var pagerAdapter: FavouritesPagerAdapter? = null private var stubBinding: ItemEmptyStateBinding? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -61,6 +58,7 @@ class FavouritesContainerFragment : pagerAdapter = adapter TabLayoutMediator(binding.tabs, binding.pager, adapter).attach() actionModeDelegate.addListener(this, viewLifecycleOwner) + addMenuProvider(FavouritesContainerMenuProvider(view.context)) viewModel.visibleCategories.observe(viewLifecycleOwner, ::onCategoriesChanged) viewModel.onError.observe(viewLifecycleOwner, ::onError) @@ -115,21 +113,6 @@ class FavouritesContainerFragment : } } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.opt_favourites, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_categories -> { - context?.let { - startActivity(CategoriesActivity.newIntent(it)) - } - true - } - else -> super.onOptionsItemSelected(item) - } - private fun onError(e: Throwable) { Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt new file mode 100644 index 000000000..1b07f535d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.favourites.ui + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity + +class FavouritesContainerMenuProvider( + private val context: Context, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_favourites, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_categories -> { + context.startActivity(CategoriesActivity.newIntent(context)) + true + } + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt index 8d4b9e419..4d55137a2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt @@ -2,18 +2,15 @@ package org.koitharu.kotatsu.favourites.ui.list import android.os.Bundle import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.view.ActionMode -import androidx.core.view.iterator import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.titleRes -import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.withArgs class FavouritesListFragment : MangaListFragment() { @@ -30,47 +27,14 @@ class FavouritesListFragment : MangaListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } + + if (categoryId != NO_ID) { + addMenuProvider(FavouritesListMenuProvider(viewModel)) + } } override fun onScrolledToEnd() = Unit - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - if (categoryId != NO_ID) { - inflater.inflate(R.menu.opt_favourites_list, menu) - menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> - for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { - val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes) - menuItem.isCheckable = true - } - submenu.setGroupCheckable(R.id.group_order, true, true) - } - } - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> - val selectedOrder = viewModel.sortOrder.value - for (item in submenu) { - val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) - item.isChecked = order == selectedOrder - } - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when { - item.itemId == R.id.action_order -> false - item.groupId == R.id.group_order -> { - val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) ?: return false - viewModel.setSortOrder(order) - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.mode_favourites, menu) return super.onCreateActionMode(mode, menu) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt new file mode 100644 index 000000000..2a1b08876 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.favourites.ui.list + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import androidx.core.view.iterator +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity + +class FavouritesListMenuProvider( + private val viewModel: FavouritesListViewModel, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_favourites_list, menu) + menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> + for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { + val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes) + menuItem.isCheckable = true + } + submenu.setGroupCheckable(R.id.group_order, true, true) + } + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> + val selectedOrder = viewModel.sortOrder.value + for (item in submenu) { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) + item.isChecked = order == selectedOrder + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when { + menuItem.itemId == R.id.action_order -> false + menuItem.groupId == R.id.group_order -> { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(menuItem.order) ?: return false + viewModel.setSortOrder(order) + true + } + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index 27f4a86ca..b68f247aa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -2,11 +2,9 @@ package org.koitharu.kotatsu.history.ui import android.os.Bundle import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.view.ActionMode -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R @@ -14,6 +12,7 @@ import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.domain.reverseAsync import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.utils.ext.addMenuProvider class HistoryListFragment : MangaListFragment() { @@ -22,6 +21,7 @@ class HistoryListFragment : MangaListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + addMenuProvider(HistoryListMenuProvider(view.context, viewModel)) viewModel.isGroupingEnabled.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } @@ -30,37 +30,6 @@ class HistoryListFragment : MangaListFragment() { override fun onScrolledToEnd() = Unit - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.opt_history, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_history_grouping)?.isChecked = - viewModel.isGroupingEnabled.value == true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_clear_history -> { - MaterialAlertDialogBuilder(context ?: return false) - .setTitle(R.string.clear_history) - .setMessage(R.string.text_clear_history_prompt) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.clear) { _, _ -> - viewModel.clearHistory() - }.show() - true - } - R.id.action_history_grouping -> { - viewModel.setGrouping(!item.isChecked) - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.mode_history, menu) return super.onCreateActionMode(mode, menu) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt new file mode 100644 index 000000000..b27629ce6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.history.ui + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koitharu.kotatsu.R + +class HistoryListMenuProvider( + private val context: Context, + private val viewModel: HistoryListViewModel, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_history, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_clear_history -> { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.clear_history) + .setMessage(R.string.text_clear_history_prompt) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + viewModel.clearHistory() + }.show() + true + } + R.id.action_history_grouping -> { + viewModel.setGrouping(!menuItem.isChecked) + true + } + else -> false + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.action_history_grouping).isChecked = viewModel.isGroupingEnabled.value == true + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index ad7e3ab70..28556ac48 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -9,6 +9,7 @@ import androidx.collection.ArraySet import androidx.core.graphics.Insets import androidx.core.view.isNotEmpty import androidx.core.view.updatePadding +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar @@ -67,11 +68,6 @@ abstract class MangaListFragment : protected val selectedItems: Set get() = collectSelectedItems() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -98,6 +94,7 @@ abstract class MangaListFragment : setOnRefreshListener(this@MangaListFragment) isEnabled = isSwipeRefreshEnabled } + addMenuProvider(MangaListMenuProvider(childFragmentManager)) viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) @@ -114,19 +111,6 @@ abstract class MangaListFragment : super.onDestroyView() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.opt_list, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_list_mode -> { - ListModeSelectDialog.show(childFragmentManager) - true - } - else -> super.onOptionsItemSelected(item) - } - override fun onItemClick(item: Manga, view: View) { if (selectionDecoration?.checkedItemsCount != 0) { selectionDecoration?.toggleItemChecked(item.id) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt new file mode 100644 index 000000000..5950cd546 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.list.ui + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import androidx.fragment.app.FragmentManager +import org.koitharu.kotatsu.R + +class MangaListMenuProvider( + private val fragmentManager: FragmentManager, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_list, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_list_mode -> { + ListModeSelectDialog.show(fragmentManager) + true + } + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index fc2dbad03..4fff18ac6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -4,7 +4,6 @@ import android.content.* import android.net.Uri import android.os.Bundle import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.activity.result.ActivityResultCallback @@ -19,6 +18,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.utils.ShareHelper +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.Progress @@ -48,6 +48,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback { - onEmptyActionClick() - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onActivityResult(result: List<@JvmSuppressWildcards Uri>) { if (result.isEmpty()) return viewModel.importFiles(result) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt new file mode 100644 index 000000000..ce9941293 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.local.ui + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import org.koitharu.kotatsu.R + +class LocalListMenuProvider( + private val onImportClick: Function0, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_local, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_import -> { + onImportClick() + true + } + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt index 556a03cac..406fbbd63 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt @@ -9,7 +9,7 @@ import android.webkit.MimeTypeMap import androidx.activity.result.contract.ActivityResultContracts import androidx.core.net.toUri -class PageSaveContract : ActivityResultContracts.CreateDocument() { +class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") { override fun createIntent(context: Context, input: String): Intent { val intent = super.createIntent(context, input) diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 16a3b16a6..6dcd26b0f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -1,9 +1,12 @@ package org.koitharu.kotatsu.remotelist.ui +import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.View import androidx.appcompat.view.ActionMode +import androidx.core.view.MenuProvider import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R @@ -11,6 +14,7 @@ import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.serializableArgument import org.koitharu.kotatsu.utils.ext.withArgs @@ -22,31 +26,15 @@ class RemoteListFragment : MangaListFragment() { private val source by serializableArgument(ARG_SOURCE) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + addMenuProvider(RemoteListMenuProvider()) + } + override fun onScrolledToEnd() { viewModel.loadNextPage() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_list_remote, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_source_settings -> { - startActivity( - SettingsActivity.newSourceSettingsIntent(context ?: return false, source) - ) - true - } - R.id.action_filter -> { - onFilterClick() - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.mode_remote, menu) return super.onCreateActionMode(mode, menu) @@ -60,6 +48,25 @@ class RemoteListFragment : MangaListFragment() { viewModel.resetFilter() } + private inner class RemoteListMenuProvider: MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_list_remote, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_source_settings -> { + startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), source)) + true + } + R.id.action_filter -> { + onFilterClick() + true + } + else -> false + } + } + companion object { private const val ARG_SOURCE = "provider" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index 88f30c87b..b309a588b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -23,15 +23,16 @@ class BackupDialogFragment : AlertDialogFragment() { private val viewModel by viewModel() private var backup: File? = null - private val saveFileContract = - registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri -> - val file = backup - if (uri != null && file != null) { - saveBackup(file, uri) - } else { - dismiss() - } + private val saveFileContract = registerForActivityResult( + ActivityResultContracts.CreateDocument("*/*") + ) { uri -> + val file = backup + if (uri != null && file != null) { + saveBackup(file, uri) + } else { + dismiss() } + } override fun onInflateView( inflater: LayoutInflater, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index e89e20173..e8044fdde 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.* import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets +import androidx.core.view.MenuProvider import androidx.core.view.updatePadding import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView @@ -20,12 +21,11 @@ import org.koitharu.kotatsu.settings.SourceSettingsFragment import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem +import org.koitharu.kotatsu.utils.ext.addMenuProvider class SourcesSettingsFragment : BaseFragment(), SourceConfigListener, - SearchView.OnQueryTextListener, - MenuItem.OnActionExpandListener, RecyclerViewOwner { private var reorderHelper: ItemTouchHelper? = null @@ -34,11 +34,6 @@ class SourcesSettingsFragment : override val recyclerView: RecyclerView get() = binding.recyclerView - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -62,6 +57,7 @@ class SourcesSettingsFragment : viewModel.items.observe(viewLifecycleOwner) { sourcesAdapter.items = it } + addMenuProvider(SourcesMenuProvider()) } override fun onDestroyView() { @@ -69,17 +65,6 @@ class SourcesSettingsFragment : super.onDestroyView() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_sources, menu) - val searchMenuItem = menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title - } - override fun onWindowInsetsChanged(insets: Insets) { binding.recyclerView.updatePadding( bottom = insets.bottom, @@ -106,21 +91,39 @@ class SourcesSettingsFragment : viewModel.expandOrCollapse(header.localeId) } - override fun onQueryTextSubmit(query: String?): Boolean = false + private inner class SourcesMenuProvider : + MenuProvider, + MenuItem.OnActionExpandListener, + SearchView.OnQueryTextListener { - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.performSearch(newText) - return true - } + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_sources, menu) + val searchMenuItem = menu.findItem(R.id.action_search) + searchMenuItem.setOnActionExpandListener(this) + val searchView = searchMenuItem.actionView as SearchView + searchView.setOnQueryTextListener(this) + searchView.setIconifiedByDefault(false) + searchView.queryHint = searchMenuItem.title + } - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { - (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) - return true - } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - (item.actionView as SearchView).setQuery("", false) - return true + override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + (item.actionView as SearchView).setQuery("", false) + return true + } + + override fun onQueryTextSubmit(query: String?): Boolean = false + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.performSearch(newText) + return true + } } private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback( diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index ceb873f49..f1af41bf3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -4,30 +4,40 @@ import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.View import androidx.appcompat.view.ActionMode +import androidx.core.view.MenuProvider import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.utils.ext.addMenuProvider class SuggestionsFragment : MangaListFragment() { override val viewModel by viewModel() override val isSwipeRefreshEnabled = false - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + addMenuProvider(SuggestionMenuProvider()) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_suggestions, menu) + override fun onScrolledToEnd() = Unit + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.mode_remote, menu) + return super.onCreateActionMode(mode, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { + private inner class SuggestionMenuProvider : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_suggestions, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { R.id.action_update -> { SuggestionsWorker.startNow(requireContext()) Snackbar.make( @@ -41,17 +51,10 @@ class SuggestionsFragment : MangaListFragment() { startActivity(SettingsActivity.newSuggestionsSettingsIntent(requireContext())) true } - else -> super.onOptionsItemSelected(item) + else -> false } } - override fun onScrolledToEnd() = Unit - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.mode_remote, menu) - return super.onCreateActionMode(mode, menu) - } - companion object { fun newInstance() = SuggestionsFragment() diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index 584e2a4ff..32120ea94 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -1,10 +1,11 @@ package org.koitharu.kotatsu.tracker.ui import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel @@ -21,6 +22,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.work.TrackWorker +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.progress.Progress @@ -37,11 +39,6 @@ class FeedFragment : private var paddingVertical = 0 private var paddingHorizontal = 0 - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -63,6 +60,7 @@ class FeedFragment : ) addItemDecoration(decoration) } + addMenuProvider(FeedMenuProvider(binding.recyclerView, viewModel)) viewModel.content.observe(viewLifecycleOwner, this::onListChanged) viewModel.onError.observe(viewLifecycleOwner, this::onError) @@ -73,36 +71,6 @@ class FeedFragment : .observe(viewLifecycleOwner, this::onUpdateProgressChanged) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_feed, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_update -> { - TrackWorker.startNow(requireContext()) - Snackbar.make( - binding.recyclerView, - R.string.feed_will_update_soon, - Snackbar.LENGTH_LONG, - ).show() - true - } - R.id.action_clear_feed -> { - MaterialAlertDialogBuilder(context ?: return false) - .setTitle(R.string.clear_updates_feed) - .setMessage(R.string.text_clear_updates_feed_prompt) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.clear) { _, _ -> - viewModel.clearFeed() - }.show() - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onDestroyView() { feedAdapter = null updateStatusSnackbar = null diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt new file mode 100644 index 000000000..6787ff7da --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.tracker.ui + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.view.MenuProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.tracker.work.TrackWorker + +class FeedMenuProvider( + private val snackbarHost: View, + private val viewModel: FeedViewModel, +) : MenuProvider { + + private val context: Context + get() = snackbarHost.context + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_feed, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_update -> { + TrackWorker.startNow(context) + Snackbar.make( + snackbarHost, + R.string.feed_will_update_soon, + Snackbar.LENGTH_LONG, + ).show() + true + } + R.id.action_clear_feed -> { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.clear_updates_feed) + .setMessage(R.string.text_clear_updates_feed_prompt) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + viewModel.clearFeed() + }.show() + true + } + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt index d881d3b1d..d37b579e7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt @@ -2,9 +2,11 @@ package org.koitharu.kotatsu.utils.ext import android.os.Bundle import android.os.Parcelable +import androidx.core.view.MenuProvider import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import java.io.Serializable @@ -43,4 +45,8 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) { if (!manager.isStateSaved) { show(manager, tag) } +} + +fun Fragment.addMenuProvider(provider: MenuProvider) { + requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt index b07b0c41d..ac878b538 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt @@ -3,12 +3,14 @@ package org.koitharu.kotatsu.utils.ext import androidx.core.os.LocaleListCompat import java.util.* -fun LocaleListCompat.toList(): List = createList(size()) { i -> get(i) } +fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw kotlin.NoSuchElementException() + +fun LocaleListCompat.toList(): List = createList(size()) { i -> getOrThrow(i) } operator fun LocaleListCompat.iterator() = object : Iterator { private var index = 0 override fun hasNext(): Boolean = index < size() - override fun next(): Locale = get(index++) + override fun next(): Locale = getOrThrow(index++) } inline fun > LocaleListCompat.mapTo( @@ -17,7 +19,7 @@ inline fun > LocaleListCompat.mapTo( ): C { val len = size() for (i in 0 until len) { - val item = get(i) + val item = get(i) ?: continue destination.add(block(item)) } return destination From ccb31de1ba338d663f16926f5a72c2c523c5daa9 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 31 May 2022 16:22:30 +0300 Subject: [PATCH 023/213] Fix wrong tracker notifications --- .../kotatsu/core/model/MangaTracking.kt | 2 +- .../tracker/domain/TrackingRepository.kt | 25 +++++++++-------- .../kotatsu/tracker/work/TrackWorker.kt | 28 ++++++++----------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt index 77fdc5925..09af6176c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.core.model import android.os.Parcelable +import java.util.* import kotlinx.parcelize.Parcelize import org.koitharu.kotatsu.parsers.model.Manga -import java.util.* data class MangaTracking( val manga: Manga, diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index aefa9a69a..2048c38e0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -77,10 +77,11 @@ class TrackingRepository( suspend fun storeTrackResult( mangaId: Long, - knownChaptersCount: Int, - lastChapterId: Long, + knownChaptersCount: Int, // how many chapters user already seen + lastChapterId: Long, // in upstream manga newChapters: List, - previousTrackChapterId: Long + previousTrackChapterId: Long, // from previous check + saveTrackLog: Boolean, ) { db.withTransaction { val entity = TrackEntity( @@ -92,14 +93,16 @@ class TrackingRepository( lastNotifiedChapterId = newChapters.lastOrNull()?.id ?: previousTrackChapterId ) db.tracksDao.upsert(entity) - val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId } - if (foundChapters.isNotEmpty()) { - val logEntity = TrackLogEntity( - mangaId = mangaId, - chapters = foundChapters.joinToString("\n") { x -> x.name }, - createdAt = System.currentTimeMillis() - ) - db.trackLogsDao.insert(logEntity) + if (saveTrackLog && previousTrackChapterId != 0L) { + val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId } + if (foundChapters.isNotEmpty()) { + val logEntity = TrackLogEntity( + mangaId = mangaId, + chapters = foundChapters.joinToString("\n") { x -> x.name }, + createdAt = System.currentTimeMillis() + ) + db.trackLogsDao.insert(logEntity) + } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index f1e40a04b..ea249c7c4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.map import androidx.work.* import coil.ImageLoader import coil.request.ImageRequest -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent @@ -31,6 +30,7 @@ import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.trySetForeground import org.koitharu.kotatsu.utils.progress.Progress +import java.util.concurrent.TimeUnit class TrackWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams), KoinComponent { @@ -65,25 +65,18 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : setProgress(workData.build()) val chapters = details?.chapters ?: continue when { - track.knownChaptersCount == -1 -> { // first check + // first check or manga was empty on last check + track.knownChaptersCount <= 0 || track.lastChapterId == 0L -> { repository.storeTrackResult( mangaId = track.manga.id, knownChaptersCount = chapters.size, lastChapterId = chapters.lastOrNull()?.id ?: 0L, previousTrackChapterId = 0L, - newChapters = emptyList() + newChapters = emptyList(), + saveTrackLog = false, ) } - track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { // manga was empty on last check - repository.storeTrackResult( - mangaId = track.manga.id, - knownChaptersCount = 0, - lastChapterId = 0L, - previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = chapters - ) - showNotification(details, channelId, chapters) - } + // the same chapters count chapters.size == track.knownChaptersCount -> { if (chapters.lastOrNull()?.id == track.lastChapterId) { // manga was not updated. skip @@ -97,8 +90,9 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : mangaId = track.manga.id, knownChaptersCount = chapters.size, lastChapterId = chapters.lastOrNull()?.id ?: 0L, - previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = emptyList() + previousTrackChapterId = 0L, + newChapters = emptyList(), + saveTrackLog = false, ) } else { val newChapters = chapters.takeLast(chapters.size - knownChapter + 1) @@ -107,7 +101,8 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : knownChaptersCount = knownChapter + 1, lastChapterId = track.lastChapterId, previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = newChapters + newChapters = newChapters, + saveTrackLog = true, ) showNotification( details, @@ -125,6 +120,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : lastChapterId = track.lastChapterId, previousTrackChapterId = track.lastNotifiedChapterId, newChapters = newChapters, + saveTrackLog = true, ) showNotification( manga = track.manga, From 30c0fd600fcb61b978721c8f46347682f0d4449e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 31 May 2022 16:43:23 +0300 Subject: [PATCH 024/213] Fix global search results order --- .../core/exceptions/CompositeException.kt | 7 ++++ .../search/ui/multi/MultiSearchViewModel.kt | 37 ++++++++++++++----- 2 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt new file mode 100644 index 000000000..353ff1ca6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt @@ -0,0 +1,7 @@ +package org.koitharu.kotatsu.core.exceptions + +import org.koitharu.kotatsu.parsers.util.mapNotNullToSet + +class CompositeException(val errors: Collection) : Exception( + message = errors.mapNotNullToSet { it.message }.joinToString() +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt index 40b6a8619..adea07898 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.exceptions.CompositeException import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode @@ -78,8 +79,7 @@ class MultiSearchViewModel( listData.value = emptyList() loadingData.value = true query.postValue(q) - val errors = searchImpl(q) - listError.value = errors.firstOrNull() + searchImpl(q) } catch (e: Throwable) { listError.value = e } finally { @@ -88,25 +88,44 @@ class MultiSearchViewModel( } } - private suspend fun searchImpl(q: String): List { + private suspend fun searchImpl(q: String) { val sources = settings.getMangaSources(includeHidden = false) val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) - return coroutineScope { + val deferredList = coroutineScope { sources.map { source -> async(dispatcher) { runCatching { val list = MangaRepository(source).getList(offset = 0, query = q) - // .sortedBy { x -> x.title.levenshteinDistance(q) } .toUi(ListMode.GRID) if (list.isNotEmpty()) { - val item = MultiSearchListModel(source, list) - listData.update { x -> x + item } + MultiSearchListModel(source, list) + } else { + null } }.onFailure { it.printStackTraceDebug() - }.exceptionOrNull() + } } } - }.awaitAll().filterNotNull() + } + val errors = ArrayList() + for (deferred in deferredList) { + deferred.await() + .onSuccess { item -> + if (item != null) { + listData.update { x -> x + item } + } + }.onFailure { + errors.add(it) + } + } + if (listData.value.isNotEmpty()) { + return + } + when (errors.size) { + 0 -> Unit + 1 -> throw errors[0] + else -> throw CompositeException(errors) + } } } \ No newline at end of file From 3edfd0892a044959ee006a3f8e94570b70db9082 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 15 Jun 2022 13:35:24 +0300 Subject: [PATCH 025/213] Move tracker logic into own class --- .idea/gradle.xml | 2 +- app/build.gradle | 6 +- .../kotatsu/core/parser/DummyParser.kt | 4 +- .../kotatsu/core/db/dao/TrackLogsDao.kt | 2 +- .../koitharu/kotatsu/core/db/dao/TracksDao.kt | 3 +- .../core/exceptions/CompositeException.kt | 7 +- .../kotatsu/core/model/MangaTracking.kt | 37 +++- .../kotatsu/core/parser/MangaRepository.kt | 6 +- .../core/parser/RemoteMangaRepository.kt | 2 +- .../local/domain/LocalMangaRepository.kt | 16 +- .../koitharu/kotatsu/tracker/TrackerModule.kt | 3 + .../kotatsu/tracker/domain/Tracker.kt | 132 +++++++++++++ .../tracker/domain/TrackingRepository.kt | 11 +- .../tracker/domain/model/MangaUpdates.kt | 9 + .../kotatsu/tracker/work/TrackWorker.kt | 175 +++++------------- build.gradle | 2 +- 16 files changed, 254 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 6e5389ed9..a0de2a152 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -7,7 +7,7 @@ From 1c1bd9265e2c647b66ac00d18c91aed8e3225b9d Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 18 Jun 2022 17:04:01 +0300 Subject: [PATCH 030/213] Fix links --- .../koitharu/kotatsu/settings/utils/AboutLinksPreference.kt | 2 +- app/src/main/res/values/constants.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt index cb4e12834..b262c2859 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt @@ -41,7 +41,7 @@ class AboutLinksPreference @JvmOverloads constructor(context: Context, attrs: At setTooltip(contentDescription.toString()) setOnClickListener { openLink( - resources.getString(R.string.url_github_issues), + resources.getString(R.string.url_github), contentDescription.toString() ) } diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 424125fab..fc2a18c3d 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -1,10 +1,10 @@ - https://github.com/nv95/Kotatsu/issues + https://github.com/nv95/Kotatsu https://discord.gg/NNJ5RgVBC5 https://4pda.to/forum/index.php?showtopic=697669 - https://https://twitter.com/kotatsuapp - https://www.reddit.com/user/kotatsuapp/ + https://twitter.com/kotatsuapp + https://reddit.com/user/kotatsuapp https://hosted.weblate.org/engage/kotatsu -1 From 00dacc32dfead24860541ebbef43c10baaf45a55 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 18 Jun 2022 18:31:57 +0300 Subject: [PATCH 031/213] Update parsers --- app/build.gradle | 9 +++++---- .../java/org/koitharu/kotatsu/core/parser/DummyParser.kt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0eab1d24d..5f6c280a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 409 - versionName '3.3' + versionCode 410 + versionName '3.3.1' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -73,7 +73,7 @@ afterEvaluate { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:0ed35a4b21') { + implementation('com.github.nv95:kotatsu-parsers:dc0129c76c') { exclude group: 'org.json', module: 'json' } @@ -100,7 +100,7 @@ dependencies { implementation 'androidx.room:room-ktx:2.4.2' kapt 'androidx.room:room-compiler:2.4.2' - implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3' implementation 'com.squareup.okio:okio:3.1.0' @@ -112,6 +112,7 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' + debugImplementation 'org.jsoup:jsoup:1.15.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt index 7c6096a24..9ebcba9f4 100644 --- a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt +++ b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt @@ -1,10 +1,10 @@ package org.koitharu.kotatsu.core.parser +import java.util.* import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* -import java.util.* /** * This parser is just for parser development, it should not be used in releases From 86d8ff3c6802d15b68fb60c5a4500afc16108ef8 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 18 Jun 2022 20:16:49 +0300 Subject: [PATCH 032/213] Refactor AboutLinksPreference --- .../settings/utils/AboutLinksPreference.kt | 79 ++++++++++--------- .../org/koitharu/kotatsu/utils/ext/ViewExt.kt | 8 -- app/src/main/res/drawable/ic_4pda.xml | 4 +- app/src/main/res/drawable/ic_reddit.xml | 14 +++- 4 files changed, 54 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt index b262c2859..42d7b70c9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt @@ -1,62 +1,67 @@ package org.koitharu.kotatsu.settings.utils +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.util.AttributeSet -import androidx.core.content.ContextCompat.startActivity +import android.view.View +import androidx.appcompat.widget.TooltipCompat import androidx.core.net.toUri import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.setTooltip +import org.koitharu.kotatsu.databinding.PreferenceAboutLinksBinding -class AboutLinksPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - Preference(context, attrs) { +class AboutLinksPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : Preference(context, attrs), View.OnClickListener { init { layoutResource = R.layout.preference_about_links isSelectable = false + isPersistent = false } override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) - holder.findViewById(R.id.btn_4pda).apply { - setTooltip(contentDescription.toString()) - setOnClickListener { openLink(resources.getString(R.string.url_forpda), contentDescription.toString()) } - } - holder.findViewById(R.id.btn_discord).apply { - setTooltip(contentDescription.toString()) - setOnClickListener { openLink(resources.getString(R.string.url_discord), contentDescription.toString()) } - } - holder.findViewById(R.id.btn_twitter).apply { - setTooltip(contentDescription.toString()) - setOnClickListener { openLink(resources.getString(R.string.url_twitter), contentDescription.toString()) } - } - holder.findViewById(R.id.btn_reddit).apply { - setTooltip(contentDescription.toString()) - setOnClickListener { openLink(resources.getString(R.string.url_reddit), contentDescription.toString()) } - } - holder.findViewById(R.id.btn_github).apply { - setTooltip(contentDescription.toString()) - setOnClickListener { - openLink( - resources.getString(R.string.url_github), - contentDescription.toString() - ) - } + val binding = PreferenceAboutLinksBinding.bind(holder.itemView) + arrayOf( + binding.btn4pda, + binding.btnDiscord, + binding.btnGithub, + binding.btnReddit, + binding.btnTwitter, + ).forEach { button -> + TooltipCompat.setTooltipText(button, button.contentDescription) + button.setOnClickListener(this) } } + override fun onClick(v: View) { + val urlResId = when (v.id) { + R.id.btn_4pda -> R.string.url_forpda + R.id.btn_discord -> R.string.url_discord + R.id.btn_twitter -> R.string.url_twitter + R.id.btn_reddit -> R.string.url_reddit + R.id.btn_github -> R.string.url_github + else -> return + } + openLink(v.context.getString(urlResId), v.contentDescription) + } + private fun openLink(url: String, title: CharSequence?) { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = url.toUri() - context.startActivity( - if (title != null) { - Intent.createChooser(intent, title) - } else { - intent - } - ) + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + try { + context.startActivity( + if (title != null) { + Intent.createChooser(intent, title) + } else { + intent + } + ) + } catch (_: ActivityNotFoundException) { + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index 56afd7cd1..67c30830e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -156,12 +156,4 @@ fun ViewGroup.findViewsByType(clazz: Class): Sequence { } } } -} - -inline fun View.setTooltip(@StringRes stringRes: Int) { - setTooltip(context.getString(stringRes)) -} - -inline fun View.setTooltip(text: String) { - TooltipCompat.setTooltipText(this, text) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_4pda.xml b/app/src/main/res/drawable/ic_4pda.xml index 6ce338a77..f0920f0d8 100644 --- a/app/src/main/res/drawable/ic_4pda.xml +++ b/app/src/main/res/drawable/ic_4pda.xml @@ -2,10 +2,10 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="1024" android:viewportHeight="1024"> + android:pathData="M426.1,112 L112,545.6l0,247.7l486.7,0l0,118.8l313.3,0L912,112ZM599.5,312L599.5,577.6L390.1,577.6Z" /> diff --git a/app/src/main/res/drawable/ic_reddit.xml b/app/src/main/res/drawable/ic_reddit.xml index d56d46d7b..047683dbc 100644 --- a/app/src/main/res/drawable/ic_reddit.xml +++ b/app/src/main/res/drawable/ic_reddit.xml @@ -1,5 +1,11 @@ - - + + From d2bbfe01f157c4c5e730a30051c6be1666f67064 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 20 Jun 2022 10:53:58 +0300 Subject: [PATCH 033/213] Add ACRA for crash reports --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 8 -- .../java/org/koitharu/kotatsu/KotatsuApp.kt | 38 ++++++++- .../koitharu/kotatsu/base/ui/BaseActivity.kt | 6 +- .../kotatsu/core/ui/AppCrashHandler.kt | 22 ----- .../koitharu/kotatsu/core/ui/CrashActivity.kt | 83 ------------------- .../ui/service/DownloadNotification.kt | 11 +-- .../kotatsu/tracker/work/TrackWorker.kt | 5 -- app/src/main/res/layout/activity_crash.xml | 64 -------------- app/src/main/res/values/constants.xml | 1 + app/src/main/res/values/strings.xml | 2 + 11 files changed, 46 insertions(+), 197 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt delete mode 100644 app/src/main/res/layout/activity_crash.xml diff --git a/app/build.gradle b/app/build.gradle index 5f6c280a0..4e4979550 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,6 +112,9 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' + implementation 'ch.acra:acra-mail:5.9.3' + implementation 'ch.acra:acra-dialog:5.9.3' + debugImplementation 'org.jsoup:jsoup:1.15.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e1688abcf..72cfa4f2c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,11 +68,6 @@ android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:windowSoftInputMode="adjustResize" /> - - diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index e00dd1680..8035a83fc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -1,9 +1,15 @@ package org.koitharu.kotatsu import android.app.Application +import android.content.Context import android.os.StrictMode import androidx.appcompat.app.AppCompatDelegate import androidx.fragment.app.strictmode.FragmentStrictMode +import org.acra.ReportField +import org.acra.config.dialog +import org.acra.config.mailSender +import org.acra.data.StringFormat +import org.acra.ktx.initAcra import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -13,7 +19,6 @@ import org.koitharu.kotatsu.core.db.databaseModule import org.koitharu.kotatsu.core.github.githubModule import org.koitharu.kotatsu.core.network.networkModule import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.AppCrashHandler import org.koitharu.kotatsu.core.ui.uiModule import org.koitharu.kotatsu.details.detailsModule import org.koitharu.kotatsu.favourites.favouritesModule @@ -41,7 +46,6 @@ class KotatsuApp : Application() { enableStrictMode() } initKoin() - Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext)) AppCompatDelegate.setDefaultNightMode(get().theme) registerActivityLifecycleCallbacks(get()) registerActivityLifecycleCallbacks(get()) @@ -75,6 +79,36 @@ class KotatsuApp : Application() { } } + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + initAcra { + buildConfigClass = BuildConfig::class.java + reportFormat = StringFormat.KEY_VALUE_LIST + reportContent = listOf( + ReportField.PACKAGE_NAME, + ReportField.APP_VERSION_CODE, + ReportField.APP_VERSION_NAME, + ReportField.ANDROID_VERSION, + ReportField.PHONE_MODEL, + ReportField.CRASH_CONFIGURATION, + ReportField.STACK_TRACE, + ReportField.SHARED_PREFERENCES, + ) + dialog { + text = getString(R.string.crash_text) + title = getString(R.string.error_occurred) + positiveButtonText = getString(R.string.send) + resIcon = R.drawable.ic_alert_outline + resTheme = android.R.style.Theme_Material_Light_Dialog_Alert + } + mailSender { + mailTo = getString(R.string.email_error_report) + reportAsFile = true + reportFileName = "stacktrace.txt" + } + } + } + private fun enableStrictMode() { StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index 9cdce9654..bd172d695 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.ActionBarContextView import androidx.appcompat.widget.Toolbar -import androidx.core.app.ActivityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -83,8 +82,9 @@ abstract class BaseActivity : override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove - ActivityCompat.recreate(this) - return true + // ActivityCompat.recreate(this) + throw RuntimeException("Test crash") + // return true } return super.onKeyDown(keyCode, event) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt deleted file mode 100644 index fb3216cb2..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.koitharu.kotatsu.core.ui - -import android.content.Context -import android.content.Intent -import android.util.Log -import kotlin.system.exitProcess -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug - -class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler { - - override fun uncaughtException(t: Thread, e: Throwable) { - val intent = CrashActivity.newIntent(applicationContext, e) - intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - try { - applicationContext.startActivity(intent) - } catch (t: Throwable) { - t.printStackTraceDebug() - } - Log.e("CRASH", e.message, e) - exitProcess(1) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt deleted file mode 100644 index 7d4d31878..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/CrashActivity.kt +++ /dev/null @@ -1,83 +0,0 @@ -package org.koitharu.kotatsu.core.ui - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.databinding.ActivityCrashBinding -import org.koitharu.kotatsu.main.ui.MainActivity -import org.koitharu.kotatsu.parsers.util.ellipsize -import org.koitharu.kotatsu.utils.ShareHelper - -class CrashActivity : Activity(), View.OnClickListener { - - private lateinit var binding: ActivityCrashBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityCrashBinding.inflate(layoutInflater) - setContentView(binding.root) - binding.textView.text = intent.getStringExtra(Intent.EXTRA_TEXT) - binding.buttonClose.setOnClickListener(this) - binding.buttonRestart.setOnClickListener(this) - binding.buttonReport.setOnClickListener(this) - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.opt_crash, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_share -> { - ShareHelper(this).shareText(binding.textView.text.toString()) - } - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun onClick(v: View) { - when (v.id) { - R.id.button_close -> { - finish() - } - R.id.button_restart -> { - val intent = Intent(applicationContext, MainActivity::class.java) - intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - finish() - } - R.id.button_report -> { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse("https://github.com/nv95/Kotatsu/issues") - try { - startActivity(Intent.createChooser(intent, getString(R.string.report_github))) - } catch (_: ActivityNotFoundException) { - } - } - } - } - - companion object { - - private const val MAX_TRACE_SIZE = 131071 - - fun newIntent(context: Context, error: Throwable): Intent { - val crashInfo = error - .stackTraceToString() - .trimIndent() - .ellipsize(MAX_TRACE_SIZE) - val intent = Intent(context, CrashActivity::class.java) - intent.putExtra(Intent.EXTRA_TEXT, crashInfo) - return intent - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt index a424e7086..a8f0744bd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt @@ -11,8 +11,8 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap +import com.google.android.material.R as materialR import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.CrashActivity import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.ui.DownloadsActivity @@ -20,7 +20,6 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import com.google.android.material.R as materialR class DownloadNotification(private val context: Context, startId: Int) { @@ -92,14 +91,6 @@ class DownloadNotification(private val context: Context, startId: Int) { builder.setContentText(message) builder.setAutoCancel(true) builder.setOngoing(false) - builder.setContentIntent( - PendingIntent.getActivity( - context, - state.manga.hashCode(), - CrashActivity.newIntent(context, state.error), - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - ) builder.setCategory(NotificationCompat.CATEGORY_ERROR) builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 5545b7562..51aedc866 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -216,10 +216,5 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : works.any { x -> x.state == WorkInfo.State.RUNNING } } } - - suspend fun getInfo(context: Context): List { - val query = WorkQuery.Builder.fromTags(listOf(TAG, TAG_ONESHOT)).build() - return WorkManager.getInstance(context).getWorkInfos(query).await().orEmpty() - } } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_crash.xml b/app/src/main/res/layout/activity_crash.xml deleted file mode 100644 index f6a974b44..000000000 --- a/app/src/main/res/layout/activity_crash.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - -