diff --git a/app/build.gradle b/app/build.gradle index 9833396f6..9f9f8a02c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,6 +70,7 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.2.0-beta01' implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06' @@ -88,6 +89,9 @@ dependencies { implementation 'com.squareup.okio:okio:2.9.0' implementation 'org.jsoup:jsoup:1.13.1' + implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0' + implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:4.3.0' + implementation 'org.koin:koin-android:2.2.0' implementation 'org.koin:koin-android-viewmodel:2.2.0' implementation 'io.coil-kt:coil-base:1.0.0' @@ -97,6 +101,6 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5' testImplementation 'junit:junit:4.13.1' - testImplementation 'org.json:json:20200518' + testImplementation 'org.json:json:20201115' testImplementation 'org.koin:koin-test:2.2.0-rc-2' } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnListItemClickListener.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnListItemClickListener.kt new file mode 100644 index 000000000..f39b81d14 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnListItemClickListener.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.base.ui.list + +import android.view.View + +interface OnListItemClickListener { + + fun onItemClick(item: I, view: View) + + fun onItemLongClick(item: I, view: View) = false +} \ No newline at end of file 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 ceb7fc8d3..05485b754 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 @@ -7,6 +7,9 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.collection.arraySetOf import androidx.core.content.edit import androidx.preference.PreferenceManager +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.flow.callbackFlow import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.utils.delegates.prefs.* @@ -117,6 +120,16 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : prefs.unregisterOnSharedPreferenceChangeListener(listener) } + fun observe() = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + sendBlocking(key) + } + prefs.registerOnSharedPreferenceChangeListener(listener) + awaitClose { + prefs.unregisterOnSharedPreferenceChangeListener(listener) + } + } + companion object { const val PAGE_SWITCH_TAPS = "taps" 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 203b7bd4f..70934ea32 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt @@ -7,5 +7,5 @@ import org.koitharu.kotatsu.details.ui.DetailsViewModel val detailsModule get() = module { - viewModel { DetailsViewModel(get(), get(), get(), get(), get(), get()) } + viewModel { DetailsViewModel(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 f340e7992..84ef434e9 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 @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.OnFavouritesChangeListener import org.koitharu.kotatsu.history.domain.HistoryRepository @@ -28,8 +29,11 @@ class DetailsViewModel( private val localMangaRepository: LocalMangaRepository, private val trackingRepository: TrackingRepository, private val searchRepository: MangaSearchRepository, - private val mangaDataRepository: MangaDataRepository -) : MangaListViewModel(), OnHistoryChangeListener, OnFavouritesChangeListener { + private val mangaDataRepository: MangaDataRepository, + settings: AppSettings +) : MangaListViewModel(settings), OnHistoryChangeListener, OnFavouritesChangeListener { + + override val content = MutableLiveData>() val mangaData = MutableLiveData() val newChapters = MutableLiveData(0) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt index 7efd2c4cd..c213051f7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt @@ -12,7 +12,7 @@ val favouritesModule single { FavouritesRepository(get()) } viewModel { (categoryId: Long) -> - FavouritesListViewModel(categoryId, get()) + FavouritesListViewModel(categoryId, get(), get()) } viewModel { FavouritesCategoriesViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index d80b4ec79..c9d972417 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.favourites.data import androidx.room.* +import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaEntity @Dao @@ -10,6 +11,10 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at") abstract suspend fun findAll(): List + @Transaction + @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at") + abstract fun observeAll(): Flow> + @Transaction @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") abstract suspend fun findAll(offset: Int, limit: Int): List @@ -18,6 +23,10 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at") abstract suspend fun findAll(categoryId: Long): List + @Transaction + @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at") + abstract fun observeAll(categoryId: Long): Flow> + @Transaction @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index f364fd37d..30a8d94d4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.favourites.domain import androidx.collection.ArraySet import androidx.room.withTransaction import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity @@ -11,6 +12,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity +import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapToSet class FavouritesRepository(private val db: MangaDatabase) { @@ -20,6 +22,11 @@ class FavouritesRepository(private val db: MangaDatabase) { return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } } + fun observeAll(): Flow> { + return db.favouritesDao.observeAll() + .mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } + } + suspend fun getAllManga(offset: Int): List { val entities = db.favouritesDao.findAll(offset, 20) return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } @@ -30,6 +37,11 @@ class FavouritesRepository(private val db: MangaDatabase) { return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } } + fun observeAll(categoryId: Long): Flow> { + return db.favouritesDao.observeAll(categoryId) + .mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } + } + suspend fun getManga(categoryId: Long, offset: Int): List { val entities = db.favouritesDao.findAll(categoryId, offset, 20) return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } 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 a5adb19db..731d42eb7 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 @@ -20,9 +20,7 @@ class FavouritesListFragment : MangaListFragment() { private val categoryId: Long get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L - override fun onRequestMoreItems(offset: Int) { - viewModel.loadList(offset) - } + override fun onRequestMoreItems(offset: Int) = Unit override fun setUpEmptyListHolder() { textView_holder.setText( diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index fb4c3f46f..21ff731e6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -1,28 +1,34 @@ package org.koitharu.kotatsu.favourites.ui.list +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.model.toGridModel +import org.koitharu.kotatsu.list.ui.model.toListDetailedModel +import org.koitharu.kotatsu.list.ui.model.toListModel class FavouritesListViewModel( private val categoryId: Long, - private val repository: FavouritesRepository -) : MangaListViewModel() { + private val repository: FavouritesRepository, + settings: AppSettings +) : MangaListViewModel(settings) { - fun loadList(offset: Int) { - launchLoadingJob { - val list = if (categoryId == 0L) { - repository.getAllManga(offset = offset) - } else { - repository.getManga(categoryId = categoryId, offset = offset) - } - if (offset == 0) { - content.value = list - } else { - content.value = content.value.orEmpty() + list - } + override val content = combine( + if (categoryId == 0L) repository.observeAll() else repository.observeAll(categoryId), + createListModeFlow() + ) { list, mode -> + when (mode) { + ListMode.LIST -> list.map { it.toListModel() } + ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() } + ListMode.GRID -> list.map { it.toGridModel() } } - } + }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) fun removeFromFavourites(manga: Manga) { launchJob { @@ -31,7 +37,6 @@ class FavouritesListViewModel( } else { repository.removeFromCategory(manga, categoryId) } - content.value = content.value?.filterNot { it.id == manga.id } } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt index e694407e0..fe15db497 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -10,5 +10,5 @@ val historyModule get() = module { single { HistoryRepository(get()) } - viewModel { HistoryListViewModel(get(), androidContext()) } + viewModel { HistoryListViewModel(get(), androidContext(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt index a407ed64c..d121e1fe6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.history.data import androidx.room.* +import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaEntity @@ -14,6 +15,10 @@ abstract class HistoryDao { @Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset") abstract suspend fun findAll(offset: Int, limit: Int): List + @Transaction + @Query("SELECT * FROM history ORDER BY updated_at DESC") + abstract fun observeAll(): Flow> + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)") abstract suspend fun findAllManga(): List diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index 2f6a98c37..73ac1cd62 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.domain import androidx.collection.ArraySet import androidx.room.withTransaction import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -13,6 +14,7 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapToSet class HistoryRepository(private val db: MangaDatabase) : KoinComponent { @@ -24,6 +26,12 @@ class HistoryRepository(private val db: MangaDatabase) : KoinComponent { return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } } + fun observeAll(): Flow> { + return db.historyDao.observeAll().mapItems { + it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) + } + } + suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) { val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) db.withTransaction { 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 244043dca..9bf9439eb 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 @@ -18,14 +18,16 @@ class HistoryListFragment : MangaListFragment() { override val viewModel by viewModel() + init { + isSwipeRefreshEnabled = false + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved) } - override fun onRequestMoreItems(offset: Int) { - viewModel.loadList(offset) - } + override fun onRequestMoreItems(offset: Int) = Unit override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.opt_history, menu) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 08b391273..51172facd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -2,34 +2,43 @@ package org.koitharu.kotatsu.history.ui import android.content.Context import android.os.Build +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.model.toGridModel +import org.koitharu.kotatsu.list.ui.model.toListDetailedModel +import org.koitharu.kotatsu.list.ui.model.toListModel import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.SingleLiveEvent class HistoryListViewModel( private val repository: HistoryRepository, private val context: Context //todo create ShortcutRepository -) : MangaListViewModel() { + , settings: AppSettings +) : MangaListViewModel(settings) { val onItemRemoved = SingleLiveEvent() - fun loadList(offset: Int) { - launchLoadingJob { - val list = repository.getList(offset = offset) - if (offset == 0) { - content.value = list - } else { - content.value = content.value.orEmpty() + list - } + override val content = combine( + repository.observeAll(), + createListModeFlow() + ) { list, mode -> + when(mode) { + ListMode.LIST -> list.map { it.toListModel() } + ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() } + ListMode.GRID -> list.map { it.toGridModel() } } - } + }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) fun clearHistory() { launchLoadingJob { repository.clear() - content.value = emptyList() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { MangaShortcut.clearAppShortcuts(context) } @@ -39,7 +48,6 @@ class HistoryListViewModel( fun removeFromHistory(manga: Manga) { launchJob { repository.delete(manga) - content.value = content.value?.filterNot { it.id == manga.id } onItemRemoved.call(manga) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { MangaShortcut(manga).removeAppShortcut(context) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaGridHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaGridHolder.kt deleted file mode 100644 index fa9f8b8ac..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaGridHolder.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.koitharu.kotatsu.list.ui - -import android.view.ViewGroup -import coil.ImageLoader -import coil.request.Disposable -import kotlinx.android.synthetic.main.item_manga_grid.* -import org.koin.core.component.inject -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.BaseViewHolder -import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest - -class MangaGridHolder(parent: ViewGroup) : - BaseViewHolder(parent, R.layout.item_manga_grid) { - - private val coil by inject() - private var imageRequest: Disposable? = null - - override fun onBind(data: Manga, extra: MangaHistory?) { - textView_title.text = data.title - imageRequest?.dispose() - imageRequest = imageView_cover.newImageRequest(data.coverUrl) - .placeholder(R.drawable.ic_placeholder) - .fallback(R.drawable.ic_placeholder) - .error(R.drawable.ic_placeholder) - .enqueueWith(coil) - } - - override fun onRecycled() { - imageRequest?.dispose() - imageView_cover.setImageDrawable(null) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListAdapter.kt deleted file mode 100644 index 530730c9d..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListAdapter.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.koitharu.kotatsu.list.ui - -import android.view.ViewGroup -import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter -import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener -import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.core.prefs.ListMode - -class MangaListAdapter(onItemClickListener: OnRecyclerItemClickListener) : - BaseRecyclerAdapter(onItemClickListener) { - - var listMode: ListMode = ListMode.LIST - - override fun onCreateViewHolder(parent: ViewGroup) = when (listMode) { - ListMode.LIST -> MangaListHolder(parent) - ListMode.DETAILED_LIST -> MangaListDetailsHolder( - parent - ) - ListMode.GRID -> MangaGridHolder(parent) - } - - override fun onGetItemId(item: Manga) = item.id - - override fun getExtra(item: Manga, position: Int): MangaHistory? = null -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListDetailsHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListDetailsHolder.kt deleted file mode 100644 index 22fb4c3f8..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListDetailsHolder.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.koitharu.kotatsu.list.ui - -import android.annotation.SuppressLint -import android.view.ViewGroup -import androidx.core.view.isVisible -import coil.ImageLoader -import coil.request.Disposable -import kotlinx.android.synthetic.main.item_manga_list_details.* -import org.koin.core.component.inject -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.BaseViewHolder -import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.textAndVisible -import kotlin.math.roundToInt - -class MangaListDetailsHolder( - parent: ViewGroup -) : BaseViewHolder(parent, R.layout.item_manga_list_details) { - - private val coil by inject() - private var imageRequest: Disposable? = null - - @SuppressLint("SetTextI18n") - override fun onBind(data: Manga, extra: MangaHistory?) { - imageRequest?.dispose() - textView_title.text = data.title - textView_subtitle.textAndVisible = data.altTitle - imageRequest = imageView_cover.newImageRequest(data.coverUrl) - .placeholder(R.drawable.ic_placeholder) - .fallback(R.drawable.ic_placeholder) - .error(R.drawable.ic_placeholder) - .enqueueWith(coil) - if (data.rating == Manga.NO_RATING) { - textView_rating.isVisible = false - } else { - textView_rating.text = "${(data.rating * 10).roundToInt()}/10" - textView_rating.isVisible = true - } - textView_tags.text = data.tags.joinToString(", ") { - it.title - } - } - - override fun onRecycled() { - imageRequest?.dispose() - imageView_cover.setImageDrawable(null) - } -} \ 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 d9bce800f..9b16cb126 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 @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.list.ui -import android.content.SharedPreferences import android.os.Bundle import android.view.* import androidx.annotation.CallSuper @@ -9,16 +8,18 @@ import androidx.core.view.GravityCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout -import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_list.* -import org.koin.android.ext.android.inject +import org.koin.android.ext.android.get import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener -import org.koitharu.kotatsu.base.ui.list.ProgressBarAdapter import org.koitharu.kotatsu.base.ui.list.decor.ItemTypeDividerDecoration import org.koitharu.kotatsu.base.ui.list.decor.SectionItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration @@ -26,28 +27,21 @@ import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaFilter -import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.filter.FilterAdapter import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.ext.* abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), - PaginationScrollListener.Callback, OnRecyclerItemClickListener, - SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener, + PaginationScrollListener.Callback, OnListItemClickListener, OnFilterChangedListener, SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener { - private val settings by inject() - private val adapterConfig = ConcatAdapter.Config.Builder() - .setIsolateViewTypes(true) - .setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS) - .build() - private var adapter: MangaListAdapter? = null - private var progressAdapter: ProgressBarAdapter? = null private var paginationListener: PaginationScrollListener? = null + private val spanResolver: MangaListSpanResolver? = null protected var isSwipeRefreshEnabled = true protected abstract val viewModel: MangaListViewModel @@ -60,31 +54,27 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - adapter = MangaListAdapter(this) - progressAdapter = ProgressBarAdapter() + adapter = MangaListAdapter(get(), this) paginationListener = PaginationScrollListener(4, this) recyclerView.setHasFixedSize(true) - initListMode(settings.listMode) + recyclerView.adapter = adapter recyclerView.addOnScrollListener(paginationListener!!) swipeRefreshLayout.setOnRefreshListener(this) recyclerView_filter.setHasFixedSize(true) recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context)) recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this)) - settings.subscribe(this) - if (savedInstanceState == null) { - onRequestMoreItems(0) - } + viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter) viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) + viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) + viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) } override fun onDestroyView() { adapter = null - progressAdapter = null paginationListener = null - settings.unsubscribe(this) super.onDestroyView() } @@ -111,11 +101,11 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), super.onPrepareOptionsMenu(menu) } - override fun onItemClick(item: Manga, position: Int, view: View) { + override fun onItemClick(item: Manga, view: View) { startActivity(DetailsActivity.newIntent(context ?: return, item)) } - override fun onItemLongClick(item: Manga, position: Int, view: View): Boolean { + override fun onItemLongClick(item: Manga, view: View): Boolean { val menu = PopupMenu(context ?: return false, view) onCreatePopupMenu(menu.menuInflater, menu.menu, item) return if (menu.menu.hasVisibleItems()) { @@ -135,16 +125,14 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), onRequestMoreItems(0) } - private fun onListChanged(list: List) { - paginationListener?.reset() - adapter?.replaceData(list) + private fun onListChanged(list: List) { + adapter?.items = list if (list.isEmpty()) { setUpEmptyListHolder() layout_holder.isVisible = true } else { layout_holder.isVisible = false } - progressAdapter?.isProgressVisible = list.isNotEmpty() recyclerView.callOnScrollListeners() } @@ -179,17 +167,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), } } - @CallSuper - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - if (context == null) { - return - } - when (key) { - AppSettings.KEY_LIST_MODE -> initListMode(settings.listMode) - AppSettings.KEY_GRID_SIZE -> UiUtils.SpanCountResolver.update(recyclerView) - } - } - protected fun onInitFilter(config: MangaFilterConfig) { recyclerView_filter.adapter = FilterAdapter( sortOrders = config.sortOrders, @@ -220,14 +197,43 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), textView_holder.setText(R.string.nothing_found) } + private fun onGridScaleChanged(scale: Float) { + UiUtils.SpanCountResolver.update(recyclerView) + } + + private fun onListModeChanged(mode: ListMode) { + with(recyclerView) { + clearItemDecorations() + when (mode) { + ListMode.LIST -> { + layoutManager = LinearLayoutManager(context) + addItemDecoration( + DividerItemDecoration( + context, + RecyclerView.VERTICAL + ) + ) + } + ListMode.DETAILED_LIST -> { + layoutManager = LinearLayoutManager(context) + } + ListMode.GRID -> { + layoutManager = GridLayoutManager(context, 3) + addItemDecoration( + SpacingItemDecoration( + resources.getDimensionPixelOffset(R.dimen.grid_spacing) + ) + ) + } + } + } + } + private fun initListMode(mode: ListMode) { val ctx = context ?: return - val position = recyclerView.firstItem - recyclerView.adapter = null recyclerView.layoutManager = null recyclerView.clearItemDecorations() recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) - adapter?.listMode = mode recyclerView.layoutManager = when (mode) { ListMode.GRID -> { GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply { @@ -239,8 +245,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), } else -> LinearLayoutManager(ctx) } - recyclerView.recycledViewPool.clear() - recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter) recyclerView.addItemDecoration( when (mode) { ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) @@ -254,7 +258,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver) } adapter?.notifyDataSetChanged() - recyclerView.firstItem = position } override fun getItemsCount() = adapter?.itemCount ?: 0 diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListHolder.kt deleted file mode 100644 index 8ebf6f91a..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListHolder.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.koitharu.kotatsu.list.ui - -import android.view.ViewGroup -import coil.ImageLoader -import coil.request.Disposable -import kotlinx.android.synthetic.main.item_manga_list.* -import org.koin.core.component.inject -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.BaseViewHolder -import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.textAndVisible - -class MangaListHolder( - parent: ViewGroup -) : BaseViewHolder(parent, R.layout.item_manga_list) { - - private val coil by inject() - private var imageRequest: Disposable? = null - - override fun onBind(data: Manga, extra: MangaHistory?) { - imageRequest?.dispose() - textView_title.text = data.title - textView_subtitle.textAndVisible = data.tags.joinToString(", ") { it.title } - imageRequest = imageView_cover.newImageRequest(data.coverUrl) - .placeholder(R.drawable.ic_placeholder) - .fallback(R.drawable.ic_placeholder) - .error(R.drawable.ic_placeholder) - .enqueueWith(coil) - } - - override fun onRecycled() { - imageRequest?.dispose() - imageView_cover.setImageDrawable(null) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSheet.kt index f4126ed4e..743f29b20 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSheet.kt @@ -6,44 +6,42 @@ import android.view.MenuItem import android.view.View import androidx.appcompat.widget.Toolbar import androidx.core.view.isVisible -import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.sheet_list.* +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet -import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener -import org.koitharu.kotatsu.base.ui.list.ProgressBarAdapter import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.ext.* abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), - PaginationScrollListener.Callback, OnRecyclerItemClickListener, + PaginationScrollListener.Callback, OnListItemClickListener, SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener { private val settings by inject() - private val adapterConfig = ConcatAdapter.Config.Builder() - .setIsolateViewTypes(true) - .setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS) - .build() private var adapter: MangaListAdapter? = null - private var progressAdapter: ProgressBarAdapter? = null protected abstract val viewModel: MangaListViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - adapter = MangaListAdapter(this) - progressAdapter = ProgressBarAdapter() + adapter = MangaListAdapter(get(), this) initListMode(settings.listMode) recyclerView.adapter = adapter recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) @@ -64,12 +62,12 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) + viewModel.listMode.observe(viewLifecycleOwner, ::initListMode) } override fun onDestroyView() { settings.unsubscribe(this) adapter = null - progressAdapter = null super.onDestroyView() } @@ -120,14 +118,13 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), } } - override fun onItemClick(item: Manga, position: Int, view: View) { + override fun onItemClick(item: Manga, view: View) { startActivity(DetailsActivity.newIntent(context ?: return, item)) } - private fun onListChanged(list: List) { - adapter?.replaceData(list) + private fun onListChanged(list: List) { + adapter?.items = list textView_holder.isVisible = list.isEmpty() - progressAdapter?.isProgressVisible = list.isNotEmpty() recyclerView.callOnScrollListeners() } @@ -147,11 +144,9 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), private fun initListMode(mode: ListMode) { val ctx = context ?: return val position = recyclerView.firstItem - recyclerView.adapter = null recyclerView.layoutManager = null recyclerView.clearItemDecorations() recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) - adapter?.listMode = mode recyclerView.layoutManager = when (mode) { ListMode.GRID -> { GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply { @@ -163,7 +158,6 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), } else -> LinearLayoutManager(ctx) } - recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter) recyclerView.addItemDecoration( when (mode) { ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSpanResolver.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSpanResolver.kt new file mode 100644 index 000000000..0bd2beb43 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSpanResolver.kt @@ -0,0 +1,55 @@ +package org.koitharu.kotatsu.list.ui + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter +import kotlin.math.abs +import kotlin.math.roundToInt + +class MangaListSpanResolver( + context: Context, + private val adapter: MangaListAdapter +) : GridLayoutManager.SpanSizeLookup(), View.OnLayoutChangeListener { + + private val gridWidth = context.resources.getDimension(R.dimen.preferred_grid_width) + private var cellWidth = -1f + + override fun getSpanSize(position: Int) = when(adapter.getItemViewType(position)) { + else -> 1 + } + + override fun onLayoutChange( + v: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + if (cellWidth <= 0f) { + return + } + val rv = v as? RecyclerView ?: return + val width = abs(right - left) + if (width == 0) { + return + } + (rv.layoutManager as? GridLayoutManager)?.spanCount = resolveGridSpanCount(width) + } + + fun setGridSize(gridSize: Int) { + val scaleFactor = gridSize / 100f + cellWidth = gridWidth * scaleFactor + } + + private fun resolveGridSpanCount(width: Int): Int { + val estimatedCount = (width / cellWidth).roundToInt() + return estimatedCount.coerceAtLeast(2) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 667909222..9f8afb6a7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -1,11 +1,31 @@ package org.koitharu.kotatsu.list.ui +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode -abstract class MangaListViewModel : BaseViewModel() { +abstract class MangaListViewModel( + private val settings: AppSettings +) : BaseViewModel() { - val content = MutableLiveData>() + abstract val content: LiveData> val filter = MutableLiveData() + val listMode = MutableLiveData() + val gridScale = settings.observe() + .filter { it == AppSettings.KEY_GRID_SIZE } + .map { settings.gridSize / 100f } + .asLiveData(viewModelScope.coroutineContext + Dispatchers.IO) + + protected fun createListModeFlow() = settings.observe() + .filter { it == AppSettings.KEY_LIST_MODE } + .map { settings.listMode } + .onStart { emit(settings.listMode) } + .distinctUntilChanged() + .onEach { listMode.postValue(it) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/IndeterminateProgressAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/IndeterminateProgressAD.kt new file mode 100644 index 000000000..1b7a3fd17 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/IndeterminateProgressAD.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress + +fun indeterminateProgressAD() = adapterDelegate(R.layout.item_progress) { +} \ No newline at end of file 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 new file mode 100644 index 000000000..85639a746 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import coil.ImageLoader +import coil.request.Disposable +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer +import kotlinx.android.synthetic.main.item_manga_list.* +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.list.ui.model.MangaGridModel +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest + +fun mangaGridItemAD( + coil: ImageLoader, + clickListener: OnListItemClickListener +) = adapterDelegateLayoutContainer(R.layout.item_manga_grid) { + + var imageRequest: Disposable? = null + + itemView.setOnClickListener { + clickListener.onItemClick(item.manga, it) + } + itemView.setOnLongClickListener { + clickListener.onItemLongClick(item.manga, it) + } + + bind { + textView_title.text = item.title + imageRequest?.dispose() + imageRequest = imageView_cover.newImageRequest(item.coverUrl) + .placeholder(R.drawable.ic_placeholder) + .fallback(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + .enqueueWith(coil) + } + + onViewRecycled { + imageRequest?.dispose() + imageView_cover.setImageDrawable(null) + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..9bbdc6d16 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.list.ui.adapter + +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.core.model.Manga +import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress +import org.koitharu.kotatsu.list.ui.model.MangaGridModel +import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel +import org.koitharu.kotatsu.list.ui.model.MangaListModel +import kotlin.jvm.internal.Intrinsics + +class MangaListAdapter( + coil: ImageLoader, + clickListener: OnListItemClickListener +) : AsyncListDifferDelegationAdapter(DiffCallback) { + + init { + delegatesManager.addDelegate(mangaListItemAD(coil, clickListener)) + .addDelegate(mangaListDetailedItemAD(coil, clickListener)) + .addDelegate(mangaGridItemAD(coil, clickListener)) + .addDelegate(indeterminateProgressAD()) + } + + private companion object DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Any, newItem: Any) = when { + oldItem is MangaListModel && newItem is MangaListModel -> { + oldItem.id == newItem.id + } + oldItem is MangaListDetailedModel && newItem is MangaListDetailedModel -> { + oldItem.id == newItem.id + } + oldItem is MangaGridModel && newItem is MangaGridModel -> { + oldItem.id == newItem.id + } + oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> { + true + } + else -> false + } + + override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean { + return Intrinsics.areEqual(oldItem, newItem) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt new file mode 100644 index 000000000..00ec8ab56 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import coil.ImageLoader +import coil.request.Disposable +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer +import kotlinx.android.synthetic.main.item_manga_list_details.* +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest +import org.koitharu.kotatsu.utils.ext.textAndVisible + +fun mangaListDetailedItemAD( + coil: ImageLoader, + clickListener: OnListItemClickListener +) = adapterDelegateLayoutContainer(R.layout.item_manga_list_details) { + + var imageRequest: Disposable? = null + + itemView.setOnClickListener { + clickListener.onItemClick(item.manga, it) + } + itemView.setOnLongClickListener { + clickListener.onItemLongClick(item.manga, it) + } + + bind { + imageRequest?.dispose() + textView_title.text = item.title + textView_subtitle.textAndVisible = item.subtitle + imageRequest = imageView_cover.newImageRequest(item.coverUrl) + .placeholder(R.drawable.ic_placeholder) + .fallback(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + .enqueueWith(coil) + textView_rating.textAndVisible = item.rating + textView_tags.text = item.tags + } + + onViewRecycled { + imageRequest?.dispose() + imageView_cover.setImageDrawable(null) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt new file mode 100644 index 000000000..9529c9a8c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -0,0 +1,44 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import coil.ImageLoader +import coil.request.Disposable +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer +import kotlinx.android.synthetic.main.item_manga_list.* +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.list.ui.model.MangaListModel +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest +import org.koitharu.kotatsu.utils.ext.textAndVisible + +fun mangaListItemAD( + coil: ImageLoader, + clickListener: OnListItemClickListener +) = adapterDelegateLayoutContainer(R.layout.item_manga_list) { + + var imageRequest: Disposable? = null + + itemView.setOnClickListener { + clickListener.onItemClick(item.manga, it) + } + itemView.setOnLongClickListener { + clickListener.onItemLongClick(item.manga, it) + } + + bind { + imageRequest?.dispose() + textView_title.text = item.title + textView_subtitle.textAndVisible = item.subtitle + imageRequest = imageView_cover.newImageRequest(item.coverUrl) + .placeholder(R.drawable.ic_placeholder) + .fallback(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + .enqueueWith(coil) + } + + onViewRecycled { + imageRequest?.dispose() + imageView_cover.setImageDrawable(null) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/IndeterminateProgress.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/IndeterminateProgress.kt new file mode 100644 index 000000000..cb739762d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/IndeterminateProgress.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.list.ui.model + +object IndeterminateProgress \ No newline at end of file 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 new file mode 100644 index 000000000..2a73c09c0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.list.ui.model + +import org.koitharu.kotatsu.core.model.Manga +import kotlin.math.roundToInt + +fun Manga.toListModel() = MangaListModel( + id = id, + title = title, + subtitle = tags.joinToString(", ") { it.title }, + coverUrl = coverUrl, + manga = this +) + +fun Manga.toListDetailedModel() = MangaListDetailedModel( + id = id, + title = title, + subtitle = altTitle, + rating = "${(rating * 10).roundToInt()}/10", + tags = tags.joinToString(", ") { it.title }, + coverUrl = coverUrl, + manga = this +) + +fun Manga.toGridModel() = MangaGridModel( + id = id, + title = title, + coverUrl = coverUrl, + manga = this +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt new file mode 100644 index 000000000..601bc9304 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.list.ui.model + +import org.koitharu.kotatsu.core.model.Manga + +data class MangaGridModel( + val id: Long, + val title: String, + val coverUrl: String, + val manga: Manga +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt new file mode 100644 index 000000000..6dac0ec99 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.list.ui.model + +import org.koitharu.kotatsu.core.model.Manga + +data class MangaListDetailedModel( + val id: Long, + val title: String, + val subtitle: String?, + val tags: String, + val coverUrl: String, + val rating: String?, + val manga: Manga +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt new file mode 100644 index 000000000..20384aaa1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.list.ui.model + +import org.koitharu.kotatsu.core.model.Manga + +data class MangaListModel( + val id: Long, + val title: String, + val subtitle: String, + val coverUrl: String, + val manga: Manga +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt index fddc72006..7a03d64ca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -2,16 +2,17 @@ package org.koitharu.kotatsu.local import org.koin.android.ext.koin.androidContext import org.koin.android.viewmodel.dsl.viewModel -import org.koin.dsl.bind +import org.koin.core.qualifier.named import org.koin.dsl.module -import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.ui.LocalListViewModel val localModule get() = module { - single { LocalMangaRepository(androidContext()) } bind MangaRepository::class + single { LocalMangaRepository(androidContext()) } + factory(named(MangaSource.LOCAL)) { get() } viewModel { LocalListViewModel(get(), get(), get(), androidContext()) } } \ No newline at end of file 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 ca94276f1..71afe9327 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,13 +3,21 @@ package org.koitharu.kotatsu.local.ui import android.content.Context import android.net.Uri import android.os.Build +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.withContext import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.model.toGridModel +import org.koitharu.kotatsu.list.ui.model.toListDetailedModel +import org.koitharu.kotatsu.list.ui.model.toListModel import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MediaStoreCompat @@ -23,9 +31,18 @@ class LocalListViewModel( private val historyRepository: HistoryRepository, private val settings: AppSettings, private val context: Context -) : MangaListViewModel() { +) : MangaListViewModel(settings) { val onMangaRemoved = SingleLiveEvent() + private val mangaList = MutableStateFlow>(emptyList()) + + override val content = combine(mangaList, createListModeFlow()) { list, mode -> + when(mode) { + ListMode.LIST -> list.map { it.toListModel() } + ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() } + ListMode.GRID -> list.map { it.toGridModel() } + } + }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) init { loadList() @@ -51,9 +68,8 @@ class LocalListViewModel( source.copyTo(output) } } ?: throw IOException("Cannot open input stream: $uri") - repository.getList(0) } - content.value = list + loadList() } } @@ -75,10 +91,9 @@ class LocalListViewModel( private fun loadList() { launchLoadingJob { - val list = withContext(Dispatchers.Default) { - repository.getList(0) + withContext(Dispatchers.Default) { + mangaList.value = repository.getList(0) } - content.value = list } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt index e806300e5..f36bf7a69 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/RemoteListModule.kt @@ -10,6 +10,6 @@ val remoteListModule get() = module { viewModel { (source: MangaSource) -> - RemoteListViewModel(get(named(source))) + RemoteListViewModel(get(named(source)), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 474bfa280..aaef4176b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -1,43 +1,70 @@ package org.koitharu.kotatsu.remotelist.ui +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.withContext import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaFilter 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.MangaFilterConfig import org.koitharu.kotatsu.list.ui.MangaListViewModel +import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress +import org.koitharu.kotatsu.list.ui.model.toGridModel +import org.koitharu.kotatsu.list.ui.model.toListDetailedModel +import org.koitharu.kotatsu.list.ui.model.toListModel class RemoteListViewModel( - private val repository: MangaRepository -) : MangaListViewModel() { + private val repository: MangaRepository, + settings: AppSettings +) : MangaListViewModel(settings) { + private val mangaList = MutableStateFlow>(emptyList()) + private val hasNextPage = MutableStateFlow(false) private var appliedFilter: MangaFilter? = null + override val content = combine(mangaList, createListModeFlow()) { list, mode -> + when(mode) { + ListMode.LIST -> list.map { it.toListModel() } + ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() } + ListMode.GRID -> list.map { it.toGridModel() } + } + }.combine(hasNextPage) { list, isHasNextPage -> + if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list + }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) + init { + loadList(0) loadFilter() } fun loadList(offset: Int) { launchLoadingJob { - val list = withContext(Dispatchers.Default) { - repository.getList( + withContext(Dispatchers.Default) { + val list = repository.getList( offset = offset, sortOrder = appliedFilter?.sortOrder, tag = appliedFilter?.tag ) - } - if (offset == 0) { - content.value = list - } else { - content.value = content.value.orEmpty() + list + if (offset == 0) { + mangaList.value = list + } else if (list.isNotEmpty()) { + mangaList.value += list + } + hasNextPage.value = list.isNotEmpty() } } } fun applyFilter(newFilter: MangaFilter) { appliedFilter = newFilter - content.value = emptyList() + mangaList.value = emptyList() + hasNextPage.value = false loadList(0) } 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 c8b764e34..b1fb6f120 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt @@ -13,6 +13,6 @@ val searchModule single { MangaSearchRepository() } - viewModel { (source: MangaSource) -> SearchViewModel(get(named(source))) } - viewModel { GlobalSearchViewModel(get()) } + viewModel { (source: MangaSource) -> SearchViewModel(get(named(source)), get()) } + viewModel { GlobalSearchViewModel(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index ffa3e1436..0b9648085 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -1,13 +1,18 @@ package org.koitharu.kotatsu.search.ui +import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel class SearchViewModel( - private val repository: MangaRepository -) : MangaListViewModel() { + private val repository: MangaRepository, + settings: AppSettings +) : MangaListViewModel(settings) { + + override val content = MutableLiveData>() fun loadList(query: String, offset: Int) { launchLoadingJob { 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 index 99154cfbf..d4b965628 100644 --- 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 @@ -1,18 +1,22 @@ package org.koitharu.kotatsu.search.ui.global +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.utils.ext.onFirst import java.io.IOException class GlobalSearchViewModel( - private val repository: MangaSearchRepository -) : MangaListViewModel() { + private val repository: MangaSearchRepository, + settings: AppSettings +) : MangaListViewModel(settings) { + override val content = MutableLiveData>() private var searchJob: Job? = null fun startSearch(query: String) { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt index b1d147bde..2ef2d1074 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt @@ -3,8 +3,6 @@ package org.koitharu.kotatsu.utils.ext import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.onEach import okhttp3.Call import okhttp3.Callback import okhttp3.Response @@ -34,16 +32,6 @@ suspend fun Call.await() = suspendCancellableCoroutine { cont -> } } -fun Flow.onFirst(action: suspend (T) -> Unit): Flow { - var isFirstCall = true - return onEach { - if (isFirstCall) { - action(it) - isFirstCall = false - } - } -} - fun CoroutineScope.launchAfter( job: Job?, context: CoroutineContext = EmptyCoroutineContext, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt new file mode 100644 index 000000000..a3de68094 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.utils.ext + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +fun Flow.onFirst(action: suspend (T) -> Unit): Flow { + var isFirstCall = true + return onEach { + if (isFirstCall) { + action(it) + isFirstCall = false + } + } +} + +inline fun Flow>.mapItems(crossinline transform: (T) -> R): Flow> { + return map { list -> list.map(transform) } +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_progress.xml b/app/src/main/res/layout/item_progress.xml index d060af2fb..2ca64e856 100644 --- a/app/src/main/res/layout/item_progress.xml +++ b/app/src/main/res/layout/item_progress.xml @@ -9,7 +9,6 @@ android:id="@+id/progressBar" style="@style/Widget.AppCompat.ProgressBar" android:layout_width="wrap_content" - android:visibility="invisible" android:layout_height="wrap_content" android:layout_gravity="center" /> diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2b52b965d..1bcb648f4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Nov 15 14:08:38 EET 2020 +#Thu Nov 19 06:56:48 EET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip