Bookmarks screen
This commit is contained in:
@@ -58,11 +58,14 @@
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
||||
android:label="@string/search_manga" />
|
||||
<activity
|
||||
android:name=".history.ui.HistoryActivity"
|
||||
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
||||
android:label="@string/history" />
|
||||
<activity
|
||||
android:name=".favourites.ui.FavouritesActivity"
|
||||
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
||||
android:label="@string/favourites" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity"
|
||||
android:label="@string/bookmarks" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
|
||||
fun interface ReversibleHandle {
|
||||
@@ -10,7 +11,11 @@ fun interface ReversibleHandle {
|
||||
}
|
||||
|
||||
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
|
||||
reverse()
|
||||
runCatching {
|
||||
reverse()
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
|
||||
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
|
||||
|
||||
@@ -98,6 +98,24 @@ class SectionedSelectionController<T : Any>(
|
||||
} != null
|
||||
}
|
||||
|
||||
fun getSectionCount(section: T): Int {
|
||||
return decorations[section]?.checkedItemsCount ?: 0
|
||||
}
|
||||
|
||||
fun addToSelection(section: T, ids: Collection<Long>): Boolean {
|
||||
val decoration = getDecoration(section)
|
||||
startActionMode()
|
||||
return actionMode?.also {
|
||||
decoration.checkAll(ids)
|
||||
notifySelectionChanged()
|
||||
} != null
|
||||
}
|
||||
|
||||
fun clearSelection(section: T) {
|
||||
decorations[section]?.clearSelection() ?: return
|
||||
notifySelectionChanged()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
return callback.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.koitharu.kotatsu.bookmarks
|
||||
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksViewModel
|
||||
|
||||
val bookmarksModule
|
||||
get() = module {
|
||||
|
||||
factory { BookmarksRepository(get()) }
|
||||
|
||||
viewModel { BookmarksViewModel(get()) }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.koitharu.kotatsu.bookmarks.data
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
class BookmarkWithManga(
|
||||
@Embedded val bookmark: BookmarkEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "manga_id"
|
||||
)
|
||||
val manga: MangaEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "tag_id",
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>,
|
||||
)
|
||||
@@ -1,20 +1,27 @@
|
||||
package org.koitharu.kotatsu.bookmarks.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||
|
||||
@Dao
|
||||
abstract class BookmarksDao {
|
||||
|
||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
|
||||
|
||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
||||
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
|
||||
|
||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
|
||||
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at"
|
||||
)
|
||||
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
||||
|
||||
@Insert
|
||||
abstract suspend fun insert(entity: BookmarkEntity)
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
package org.koitharu.kotatsu.bookmarks.data
|
||||
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.util.*
|
||||
|
||||
fun BookmarkWithManga.toBookmark() = bookmark.toBookmark(
|
||||
manga.toManga(tags.toMangaTags())
|
||||
)
|
||||
|
||||
fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark(
|
||||
manga = manga,
|
||||
pageId = pageId,
|
||||
@@ -30,4 +24,10 @@ fun Bookmark.toEntity() = BookmarkEntity(
|
||||
imageUrl = imageUrl,
|
||||
createdAt = createdAt.time,
|
||||
percent = percent,
|
||||
)
|
||||
)
|
||||
|
||||
fun Collection<BookmarkEntity>.toBookmarks(manga: Manga) = map {
|
||||
it.toBookmark(manga)
|
||||
}
|
||||
|
||||
fun Collection<Bookmark>.ids() = map { it.pageId }
|
||||
@@ -1,15 +1,21 @@
|
||||
package org.koitharu.kotatsu.bookmarks.domain
|
||||
|
||||
import android.database.SQLException
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.bookmarks.data.toBookmark
|
||||
import org.koitharu.kotatsu.bookmarks.data.toBookmarks
|
||||
import org.koitharu.kotatsu.bookmarks.data.toEntity
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
|
||||
class BookmarksRepository(
|
||||
private val db: MangaDatabase,
|
||||
@@ -23,6 +29,17 @@ class BookmarksRepository(
|
||||
return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) }
|
||||
}
|
||||
|
||||
fun observeBookmarks(): Flow<Map<Manga, List<Bookmark>>> {
|
||||
return db.bookmarksDao.observe().map { map ->
|
||||
val res = LinkedHashMap<Manga, List<Bookmark>>(map.size)
|
||||
for ((k, v) in map) {
|
||||
val manga = k.toManga()
|
||||
res[manga] = v.toBookmarks(manga)
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addBookmark(bookmark: Bookmark) {
|
||||
db.withTransaction {
|
||||
val tags = bookmark.manga.tags.toEntities()
|
||||
@@ -35,4 +52,38 @@ class BookmarksRepository(
|
||||
suspend fun removeBookmark(mangaId: Long, pageId: Long) {
|
||||
db.bookmarksDao.delete(mangaId, pageId)
|
||||
}
|
||||
|
||||
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
|
||||
val entities = ArrayList<BookmarkEntity>(ids.size)
|
||||
db.withTransaction {
|
||||
val dao = db.bookmarksDao
|
||||
for ((manga, idSet) in ids) {
|
||||
for (pageId in idSet) {
|
||||
val e = dao.find(manga.id, pageId)
|
||||
if (e != null) {
|
||||
entities.add(e)
|
||||
}
|
||||
dao.delete(manga.id, pageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return BookmarksRestorer(entities)
|
||||
}
|
||||
|
||||
private inner class BookmarksRestorer(
|
||||
private val entities: Collection<BookmarkEntity>,
|
||||
) : ReversibleHandle {
|
||||
|
||||
override suspend fun reverse() {
|
||||
db.withTransaction {
|
||||
for (e in entities) {
|
||||
try {
|
||||
db.bookmarksDao.insert(e)
|
||||
} catch (e: SQLException) {
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.commit
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||
|
||||
class BookmarksActivity : BaseActivity<ActivityContainerBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityContainerBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val fm = supportFragmentManager
|
||||
if (fm.findFragmentById(R.id.container) == null) {
|
||||
fm.commit {
|
||||
val fragment = BookmarksFragment.newInstance()
|
||||
replace(R.id.container, fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
with(binding.toolbar) {
|
||||
updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.bookmarks.data.ids
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
|
||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
||||
|
||||
class BookmarksFragment : BaseFragment<FragmentListSimpleBinding>(), ListStateHolderListener,
|
||||
OnListItemClickListener<Bookmark>, SectionedSelectionController.Callback<Manga> {
|
||||
|
||||
private val viewModel by viewModel<BookmarksViewModel>()
|
||||
private var adapter: BookmarksGroupAdapter? = null
|
||||
private var selectionController: SectionedSelectionController<Manga>? = null
|
||||
|
||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
|
||||
return FragmentListSimpleBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
selectionController = SectionedSelectionController(
|
||||
activity = requireActivity(),
|
||||
registryOwner = this,
|
||||
callback = this,
|
||||
)
|
||||
adapter = BookmarksGroupAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
coil = get(),
|
||||
listener = this,
|
||||
selectionController = checkNotNull(selectionController),
|
||||
bookmarkClickListener = this,
|
||||
groupClickListener = OnGroupClickListener(),
|
||||
)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
adapter = null
|
||||
selectionController = null
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
|
||||
val intent = ReaderActivity.newIntent(view.context, item)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(item.manga, item.pageId) ?: false
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
|
||||
override fun onEmptyActionClick() = Unit
|
||||
|
||||
override fun onSelectionChanged(count: Int) {
|
||||
binding.recyclerView.invalidateNestedItemDecorations()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.title = selectionController?.count?.toString()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_remove -> {
|
||||
val ids = selectionController?.snapshot() ?: return false
|
||||
viewModel.removeBookmarks(ids)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateItemDecoration(section: Manga): AbstractSelectionItemDecoration {
|
||||
return BookmarksSelectionDecoration(requireContext())
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
binding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<ListModel>) {
|
||||
adapter?.items = list
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
e.getDisplayMessage(resources),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun onActionDone(action: ReversibleAction) {
|
||||
val handle = action.handle
|
||||
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
||||
val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
|
||||
if (handle != null) {
|
||||
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
|
||||
}
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private inner class OnGroupClickListener : OnListItemClickListener<BookmarksGroup> {
|
||||
|
||||
override fun onItemClick(item: BookmarksGroup, view: View) {
|
||||
val controller = selectionController
|
||||
if (controller != null && controller.count > 0) {
|
||||
if (controller.getSectionCount(item.manga) == item.bookmarks.size) {
|
||||
controller.clearSelection(item.manga)
|
||||
} else {
|
||||
controller.addToSelection(item.manga, item.bookmarks.ids())
|
||||
}
|
||||
return
|
||||
}
|
||||
val intent = DetailsActivity.newIntent(view.context, item.manga)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: BookmarksGroup, view: View): Boolean {
|
||||
return selectionController?.addToSelection(item.manga, item.bookmarks.ids()) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = BookmarksFragment()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.utils.ext.getItem
|
||||
|
||||
class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
|
||||
|
||||
override fun getItemId(parent: RecyclerView, child: View): Long {
|
||||
val holder = parent.getChildViewHolder(child) ?: return RecyclerView.NO_ID
|
||||
val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID
|
||||
return item.pageId
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
|
||||
class BookmarksViewModel(
|
||||
private val repository: BookmarksRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||
|
||||
val content: LiveData<List<ListModel>> = repository.observeBookmarks()
|
||||
.map { list ->
|
||||
if (list.isEmpty()) {
|
||||
listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_favourites,
|
||||
textPrimary = R.string.no_bookmarks_yet,
|
||||
textSecondary = R.string.no_bookmarks_summary,
|
||||
actionStringRes = 0,
|
||||
)
|
||||
)
|
||||
} else list.map { (manga, bookmarks) ->
|
||||
BookmarksGroup(manga, bookmarks)
|
||||
}
|
||||
}
|
||||
.catch { e -> e.toErrorState(canRetry = false) }
|
||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||
|
||||
|
||||
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = repository.removeBookmarks(ids)
|
||||
onActionDone.postCall(ReversibleAction(R.string.bookmarks_removed, handle))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
@@ -19,7 +19,7 @@ class BookmarksAdapter(
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
||||
return oldItem.manga.id == newItem.manga.id && oldItem.chapterId == newItem.chapterId
|
||||
return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.base.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
|
||||
fun bookmarksGroupAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
sharedPool: RecyclerView.RecycledViewPool,
|
||||
selectionController: SectionedSelectionController<Manga>,
|
||||
bookmarkClickListener: OnListItemClickListener<Bookmark>,
|
||||
groupClickListener: OnListItemClickListener<BookmarksGroup>,
|
||||
) = adapterDelegateViewBinding<BookmarksGroup, ListModel, ItemBookmarksGroupBinding>(
|
||||
{ layoutInflater, parent -> ItemBookmarksGroupBinding.inflate(layoutInflater, parent, false) }
|
||||
) {
|
||||
|
||||
val viewListenerAdapter = object : View.OnClickListener, View.OnLongClickListener {
|
||||
override fun onClick(v: View) = groupClickListener.onItemClick(item, v)
|
||||
override fun onLongClick(v: View) = groupClickListener.onItemLongClick(item, v)
|
||||
}
|
||||
var imageRequest: Disposable? = null
|
||||
val adapter = BookmarksAdapter(coil, lifecycleOwner, bookmarkClickListener)
|
||||
binding.recyclerView.setRecycledViewPool(sharedPool)
|
||||
binding.recyclerView.adapter = adapter
|
||||
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||
binding.root.setOnClickListener(viewListenerAdapter)
|
||||
binding.root.setOnLongClickListener(viewListenerAdapter)
|
||||
|
||||
bind { payloads ->
|
||||
if (payloads.isEmpty()) {
|
||||
binding.recyclerView.clearItemDecorations()
|
||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||
selectionController.attachToRecyclerView(item.manga, binding.recyclerView)
|
||||
}
|
||||
imageRequest = binding.imageViewCover.newImageRequest(item.manga.coverUrl)
|
||||
.referer(item.manga.publicUrl)
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
.scale(Scale.FILL)
|
||||
.allowRgb565(true)
|
||||
.lifecycle(lifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
binding.textViewTitle.text = item.manga.title
|
||||
adapter.items = item.bookmarks
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
imageRequest?.dispose()
|
||||
imageRequest = null
|
||||
CoilUtils.dispose(binding.imageViewCover)
|
||||
binding.imageViewCover.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||
import org.koitharu.kotatsu.list.ui.adapter.*
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class BookmarksGroupAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
selectionController: SectionedSelectionController<Manga>,
|
||||
listener: ListStateHolderListener,
|
||||
bookmarkClickListener: OnListItemClickListener<Bookmark>,
|
||||
groupClickListener: OnListItemClickListener<BookmarksGroup>,
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
val pool = RecyclerView.RecycledViewPool()
|
||||
delegatesManager
|
||||
.addDelegate(
|
||||
bookmarksGroupAD(
|
||||
coil = coil,
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
sharedPool = pool,
|
||||
selectionController = selectionController,
|
||||
bookmarkClickListener = bookmarkClickListener,
|
||||
groupClickListener = groupClickListener,
|
||||
)
|
||||
)
|
||||
.addDelegate(loadingStateAD())
|
||||
.addDelegate(loadingFooterAD())
|
||||
.addDelegate(emptyStateListAD(listener))
|
||||
.addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return when {
|
||||
oldItem is BookmarksGroup && newItem is BookmarksGroup -> {
|
||||
oldItem.manga.id == newItem.manga.id
|
||||
}
|
||||
else -> oldItem.javaClass == newItem.javaClass
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return Intrinsics.areEqual(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
||||
return when {
|
||||
oldItem is BookmarksGroup && newItem is BookmarksGroup -> Unit
|
||||
else -> super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.areItemsEquals
|
||||
|
||||
class BookmarksGroup(
|
||||
val manga: Manga,
|
||||
val bookmarks: List<Bookmark>,
|
||||
) : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BookmarksGroup
|
||||
|
||||
if (manga != other.manga) return false
|
||||
|
||||
return bookmarks.areItemsEquals(other.bookmarks) { a, b ->
|
||||
a.imageUrl == b.imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga.hashCode()
|
||||
result = 31 * result + bookmarks.sumOf { it.imageUrl.hashCode() }
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,43 @@ class MangaEntity(
|
||||
@ColumnInfo(name = "state") val state: String?,
|
||||
@ColumnInfo(name = "author") val author: String?,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
)
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaEntity
|
||||
|
||||
if (id != other.id) return false
|
||||
if (title != other.title) return false
|
||||
if (altTitle != other.altTitle) return false
|
||||
if (url != other.url) return false
|
||||
if (publicUrl != other.publicUrl) return false
|
||||
if (rating != other.rating) return false
|
||||
if (isNsfw != other.isNsfw) return false
|
||||
if (coverUrl != other.coverUrl) return false
|
||||
if (largeCoverUrl != other.largeCoverUrl) return false
|
||||
if (state != other.state) return false
|
||||
if (author != other.author) return false
|
||||
if (source != other.source) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + (altTitle?.hashCode() ?: 0)
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + publicUrl.hashCode()
|
||||
result = 31 * result + rating.hashCode()
|
||||
result = 31 * result + isNsfw.hashCode()
|
||||
result = 31 * result + coverUrl.hashCode()
|
||||
result = 31 * result + (largeCoverUrl?.hashCode() ?: 0)
|
||||
result = 31 * result + (state?.hashCode() ?: 0)
|
||||
result = 31 * result + (author?.hashCode() ?: 0)
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,23 @@ class MangaWithTags(
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>,
|
||||
)
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaWithTags
|
||||
|
||||
if (manga != other.manga) return false
|
||||
if (tags != other.tags) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,27 @@ class TagEntity(
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "key") val key: String,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
)
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TagEntity
|
||||
|
||||
if (id != other.id) return false
|
||||
if (title != other.title) return false
|
||||
if (key != other.key) return false
|
||||
if (source != other.source) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + key.hashCode()
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.app.ActivityOptions
|
||||
import android.os.Bundle
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
@@ -10,7 +9,6 @@ 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
|
||||
@@ -21,7 +19,6 @@ import coil.size.Scale
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -30,7 +27,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet
|
||||
|
||||
@@ -13,11 +13,13 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
|
||||
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
||||
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
|
||||
import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
@@ -74,6 +76,8 @@ class ExploreFragment : BaseFragment<FragmentExploreBinding>(),
|
||||
override fun onClick(v: View) {
|
||||
val intent = when (v.id) {
|
||||
R.id.button_history -> HistoryActivity.newIntent(v.context)
|
||||
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
|
||||
R.id.button_bookmarks -> BookmarksActivity.newIntent(v.context)
|
||||
else -> return
|
||||
}
|
||||
startActivity(intent)
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.*
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -31,8 +30,8 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.flattenTo
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.findViewsByType
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
||||
|
||||
class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEventListener,
|
||||
SectionedSelectionController.Callback<LibrarySectionModel> {
|
||||
@@ -141,7 +140,7 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(count: Int) {
|
||||
invalidateItemDecorations()
|
||||
binding.recyclerView.invalidateNestedItemDecorations()
|
||||
}
|
||||
|
||||
override fun onCreateItemDecoration(section: LibrarySectionModel): AbstractSelectionItemDecoration {
|
||||
@@ -164,12 +163,6 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
|
||||
return viewModel.getManga(snapshot.values.flattenTo(HashSet()))
|
||||
}
|
||||
|
||||
private fun invalidateItemDecorations() {
|
||||
binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach {
|
||||
it.invalidateItemDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<ListModel>) {
|
||||
adapter?.items = list
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.ActivityContainerBinding
|
||||
import org.koitharu.kotatsu.local.ui.LocalListFragment
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
@@ -35,7 +36,11 @@ class MangaListActivity : BaseActivity<ActivityContainerBinding>() {
|
||||
return
|
||||
}
|
||||
fm.commit {
|
||||
val fragment = RemoteListFragment.newInstance(source)
|
||||
val fragment = if (source == MangaSource.LOCAL) {
|
||||
LocalListFragment.newInstance()
|
||||
} else {
|
||||
RemoteListFragment.newInstance(source)
|
||||
}
|
||||
replace(R.id.container, fragment)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
runOnCommit(ApplyFilterRunnable(fragment, tags))
|
||||
|
||||
@@ -6,12 +6,9 @@ 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
|
||||
@@ -31,7 +28,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.findViewsByType
|
||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
||||
|
||||
class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaListListener,
|
||||
ListSelectionController.Callback {
|
||||
@@ -144,9 +141,7 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(count: Int) {
|
||||
binding.recyclerView.findViewsByType(RecyclerView::class.java).forEach {
|
||||
it.invalidateItemDecorations()
|
||||
}
|
||||
binding.recyclerView.invalidateNestedItemDecorations()
|
||||
}
|
||||
|
||||
private fun collectSelectedItems(): Set<Manga> {
|
||||
|
||||
@@ -138,4 +138,10 @@ fun <T : View> ViewGroup.findViewsByType(clazz: Class<T>): Sequence<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.invalidateNestedItemDecorations() {
|
||||
findViewsByType(RecyclerView::class.java).forEach {
|
||||
it.invalidateItemDecorations()
|
||||
}
|
||||
}
|
||||
17
app/src/main/res/layout/fragment_list_simple.xml
Normal file
17
app/src/main/res/layout/fragment_list_simple.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/list_spacing"
|
||||
android:paddingTop="@dimen/grid_spacing_outer"
|
||||
android:paddingRight="@dimen/list_spacing"
|
||||
android:paddingBottom="@dimen/grid_spacing_outer"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
53
app/src/main/res/layout/item_bookmarks_group.xml
Normal file
53
app/src/main/res/layout/item_bookmarks_group.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:contentPadding="@dimen/list_spacing">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="62dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="H,13:18"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/grid_spacing"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
||||
tools:listitem="@layout/item_bookmark" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
12
app/src/main/res/menu/mode_bookmarks.xml
Normal file
12
app/src/main/res/menu/mode_bookmarks.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_remove"
|
||||
android:icon="@drawable/ic_delete"
|
||||
android:title="@string/remove"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
@@ -324,4 +324,7 @@
|
||||
<string name="last_2_hours">Last 2 hours</string>
|
||||
<string name="history_cleared">History cleared</string>
|
||||
<string name="manage">Manage</string>
|
||||
<string name="no_bookmarks_yet">No bookmarks yet</string>
|
||||
<string name="no_bookmarks_summary">You can create bookmark while reading manga</string>
|
||||
<string name="bookmarks_removed">Bookmarks removed</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user