Manga updates feed
This commit is contained in:
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
object Migration5To6 : Migration(4, 5) {
|
||||
object Migration5To6 : Migration(5, 6) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
|
||||
|
||||
@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
enum class AppSection {
|
||||
|
||||
LOCAL, FAVOURITES, HISTORY;
|
||||
LOCAL, FAVOURITES, HISTORY, FEED
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.ui.main.list.favourites.FavouritesContainerFragment
|
||||
import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment
|
||||
import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment
|
||||
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
|
||||
import org.koitharu.kotatsu.ui.main.tracklogs.FeedFragment
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderActivity
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.ui.settings.AppUpdateService
|
||||
@@ -118,6 +119,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
settings.defaultSection = AppSection.LOCAL
|
||||
setPrimaryFragment(LocalListFragment.newInstance())
|
||||
}
|
||||
R.id.nav_feed -> {
|
||||
settings.defaultSection = AppSection.FEED
|
||||
setPrimaryFragment(FeedFragment.newInstance())
|
||||
}
|
||||
R.id.nav_action_settings -> {
|
||||
startActivity(SettingsActivity.newIntent(this))
|
||||
return true
|
||||
@@ -190,6 +195,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
navigationView.setCheckedItem(R.id.nav_history)
|
||||
setPrimaryFragment(HistoryListFragment.newInstance())
|
||||
}
|
||||
AppSection.FEED -> {
|
||||
navigationView.setCheckedItem(R.id.nav_feed)
|
||||
setPrimaryFragment(FeedFragment.newInstance())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
|
||||
adapter = MangaListAdapter(this)
|
||||
recyclerView.setHasFixedSize(true)
|
||||
initListMode(settings.listMode)
|
||||
recyclerView.adapter = adapter
|
||||
// recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
||||
swipeRefreshLayout.setOnRefreshListener(this)
|
||||
recyclerView_filter.setHasFixedSize(true)
|
||||
|
||||
@@ -102,8 +102,6 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
|
||||
|
||||
companion object {
|
||||
|
||||
private const val REQUEST_IMPORT = 50
|
||||
|
||||
fun newInstance() = LocalListFragment()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.ui.main.tracklogs
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
|
||||
|
||||
class FeedAdapter(onItemClickListener: OnRecyclerItemClickListener<TrackingLogItem>? = null) :
|
||||
BaseRecyclerAdapter<TrackingLogItem, Unit>(onItemClickListener) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<TrackingLogItem, Unit> {
|
||||
return FeedHolder(parent)
|
||||
}
|
||||
|
||||
override fun onGetItemId(item: TrackingLogItem) = item.id
|
||||
|
||||
override fun getExtra(item: TrackingLogItem, position: Int) = Unit
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.koitharu.kotatsu.ui.main.tracklogs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.fragment_tracklogs.*
|
||||
import moxy.MvpDelegate
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.ui.common.BaseFragment
|
||||
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
|
||||
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
|
||||
import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.callOnScrollListeners
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.hasItems
|
||||
|
||||
class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), FeedView,
|
||||
PaginationScrollListener.Callback, OnRecyclerItemClickListener<TrackingLogItem> {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::FeedPresenter)
|
||||
|
||||
private var adapter: FeedAdapter? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = FeedAdapter(this)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addItemDecoration(
|
||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
||||
)
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
||||
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
|
||||
onRequestMoreItems(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onListChanged(list: List<TrackingLogItem>) {
|
||||
adapter?.replaceData(list)
|
||||
if (list.isEmpty()) {
|
||||
setUpEmptyListHolder()
|
||||
layout_holder.isVisible = true
|
||||
} else {
|
||||
layout_holder.isVisible = false
|
||||
}
|
||||
recyclerView.callOnScrollListeners()
|
||||
}
|
||||
|
||||
override fun onListAppended(list: List<TrackingLogItem>) {
|
||||
adapter?.appendData(list)
|
||||
recyclerView.callOnScrollListeners()
|
||||
}
|
||||
|
||||
override fun onListError(e: Throwable) {
|
||||
if (recyclerView.hasItems) {
|
||||
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
textView_holder.text = e.getDisplayMessage(resources)
|
||||
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
0,
|
||||
R.drawable.ic_error_large,
|
||||
0,
|
||||
0
|
||||
)
|
||||
layout_holder.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasItems = recyclerView.hasItems
|
||||
progressBar.isVisible = isLoading && !hasItems
|
||||
if (isLoading) {
|
||||
layout_holder.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestMoreItems(offset: Int) {
|
||||
presenter.loadList(offset)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: TrackingLogItem, position: Int, view: View) {
|
||||
startActivity(MangaDetailsActivity.newIntent(context ?: return, item.manga))
|
||||
}
|
||||
|
||||
private fun setUpEmptyListHolder() {
|
||||
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
|
||||
textView_holder.setText(R.string.text_feed_holder)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = FeedFragment()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.ui.main.tracklogs
|
||||
|
||||
import android.view.ViewGroup
|
||||
import coil.api.clear
|
||||
import coil.api.load
|
||||
import kotlinx.android.synthetic.main.item_tracklog.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.utils.ext.format
|
||||
|
||||
class FeedHolder(parent: ViewGroup) :
|
||||
BaseViewHolder<TrackingLogItem, Unit>(parent, R.layout.item_tracklog) {
|
||||
|
||||
override fun onBind(data: TrackingLogItem, extra: Unit) {
|
||||
imageView_cover.load(data.manga.coverUrl) {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
error(R.drawable.ic_placeholder)
|
||||
}
|
||||
textView_title.text = data.manga.title
|
||||
textView_subtitle.text = data.createdAt.format("d.m.Y")
|
||||
textView_chapters.text = data.chapters.joinToString("\n")
|
||||
}
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
imageView_cover.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.koitharu.kotatsu.ui.main.tracklogs
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moxy.presenterScope
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
|
||||
import org.koitharu.kotatsu.ui.common.BasePresenter
|
||||
|
||||
class FeedPresenter : BasePresenter<FeedView>() {
|
||||
|
||||
private lateinit var repository: TrackingRepository
|
||||
|
||||
override fun onFirstViewAttach() {
|
||||
repository = TrackingRepository()
|
||||
super.onFirstViewAttach()
|
||||
}
|
||||
|
||||
fun loadList(offset: Int) {
|
||||
presenterScope.launch {
|
||||
viewState.onLoadingStateChanged(true)
|
||||
try {
|
||||
val list = withContext(Dispatchers.IO) {
|
||||
repository.getTrackingLog(offset, 20)
|
||||
}
|
||||
viewState.onListChanged(list)
|
||||
} catch (e: CancellationException) {
|
||||
} catch (e: Throwable) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
viewState.onListError(e)
|
||||
} finally {
|
||||
viewState.onLoadingStateChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.ui.main.tracklogs
|
||||
|
||||
import moxy.viewstate.strategy.AddToEndSingleTagStrategy
|
||||
import moxy.viewstate.strategy.AddToEndStrategy
|
||||
import moxy.viewstate.strategy.StateStrategyType
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.ui.common.BaseMvpView
|
||||
|
||||
interface FeedView : BaseMvpView {
|
||||
|
||||
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
|
||||
fun onListChanged(list: List<TrackingLogItem>)
|
||||
|
||||
@StateStrategyType(AddToEndStrategy::class, tag = "content")
|
||||
fun onListAppended(list: List<TrackingLogItem>)
|
||||
|
||||
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
|
||||
fun onListError(e: Throwable)
|
||||
}
|
||||
14
app/src/main/res/drawable/ic_feed.xml
Normal file
14
app/src/main/res/drawable/ic_feed.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6.18,17.82m-2.18,0a2.18,2.18 0,1 1,4.36 0a2.18,2.18 0,1 1,-4.36 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,4.44v2.83c7.03,0 12.73,5.7 12.73,12.73h2.83c0,-8.59 -6.97,-15.56 -15.56,-15.56zM4,10.1v2.83c3.9,0 7.07,3.17 7.07,7.07h2.83c0,-5.47 -4.43,-9.9 -9.9,-9.9z" />
|
||||
</vector>
|
||||
47
app/src/main/res/layout/fragment_tracklogs.xml
Normal file
47
app/src/main/res/layout/fragment_tracklogs.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
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="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
76
app/src/main/res/layout/item_tracklog.xml
Normal file
76
app/src/main/res/layout/item_tracklog.xml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:paddingBottom="12dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.koitharu.kotatsu.ui.common.widgets.CoverImageView
|
||||
android:layout_width="87dp"
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:layout_toEndOf="@id/imageView_cover"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
tools:text="@tools:sample/lorem[6]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_toEndOf="@id/imageView_cover"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@id/textView_title"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@tools:sample/lorem[6]" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_toEndOf="@id/imageView_cover"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@id/textView_subtitle"
|
||||
android:background="?android:listDivider" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_chapters"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_below="@id/divider"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_toEndOf="@id/imageView_cover"
|
||||
android:ellipsize="none"
|
||||
android:gravity="center_vertical"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:singleLine="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -14,6 +14,10 @@
|
||||
android:id="@+id/nav_history"
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/history" />
|
||||
<item
|
||||
android:id="@+id/nav_feed"
|
||||
android:icon="@drawable/ic_feed"
|
||||
android:title="@string/updates" />
|
||||
</group>
|
||||
|
||||
<item
|
||||
|
||||
@@ -135,4 +135,6 @@
|
||||
<string name="all_favourites">Всё избранное</string>
|
||||
<string name="favourites_category_empty">В этой категории ничего нет</string>
|
||||
<string name="read_later">Прочитать позже</string>
|
||||
<string name="updates">Обновления</string>
|
||||
<string name="text_feed_holder">Here you will see manga updates</string>
|
||||
</resources>
|
||||
@@ -136,4 +136,6 @@
|
||||
<string name="all_favourites">All favourites</string>
|
||||
<string name="favourites_category_empty">This category is empty</string>
|
||||
<string name="read_later">Read later</string>
|
||||
<string name="updates">Updates</string>
|
||||
<string name="text_feed_holder">Here you will see manga updates</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user