Manga updates feed

This commit is contained in:
Koitharu
2020-05-22 20:28:14 +03:00
parent 140a0f4d66
commit aec2d71688
16 changed files with 378 additions and 5 deletions

View File

@@ -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)")

View File

@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.prefs
enum class AppSection {
LOCAL, FAVOURITES, HISTORY;
LOCAL, FAVOURITES, HISTORY, FEED
}

View File

@@ -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())
}
}
}

View File

@@ -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)

View File

@@ -102,8 +102,6 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
companion object {
private const val REQUEST_IMPORT = 50
fun newInstance() = LocalListFragment()
}
}

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}

View 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>

View 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>

View 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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>