History
This commit is contained in:
@@ -54,5 +54,5 @@ class KotatsuApp : Application() {
|
||||
applicationContext,
|
||||
MangaDatabase::class.java,
|
||||
"kotatsu-db"
|
||||
)
|
||||
).fallbackToDestructiveMigration() //TODO remove
|
||||
}
|
||||
@@ -1,12 +1,41 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
|
||||
@Dao
|
||||
interface HistoryDao {
|
||||
abstract class HistoryDao {
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Transaction
|
||||
@Query("SELECT * FROM history ORDER BY :orderBy LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun getAll(offset: Int, limit: Int, orderBy: String): List<HistoryWithManga>
|
||||
|
||||
@Query("DELETE FROM history")
|
||||
abstract suspend fun clear()
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(entity: HistoryEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insertManga(manga: MangaEntity): Long
|
||||
|
||||
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, updated_at = :updatedAt WHERE manga_id = :mangaId")
|
||||
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, updatedAt: Long): Int
|
||||
|
||||
suspend fun update(entity: HistoryWithManga) = update(entity.manga.id, entity.history.page, entity.history.chapterId, entity.history.updatedAt)
|
||||
|
||||
@Transaction
|
||||
suspend open fun upsert(entity: HistoryWithManga) {
|
||||
if (update(entity) == 0) {
|
||||
insertManga(entity.manga)
|
||||
insert(entity.history)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM history")
|
||||
suspend fun getAll(): List<HistoryEntity>
|
||||
}
|
||||
@@ -3,6 +3,13 @@ package org.koitharu.kotatsu.core.db
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
@Database(entities = [HistoryEntity::class], version = 1)
|
||||
abstract class MangaDatabase : RoomDatabase()
|
||||
@Database(entities = [MangaEntity::class, TagEntity::class, HistoryEntity::class], version = 1)
|
||||
abstract class MangaDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun historyDao(): HistoryDao
|
||||
|
||||
abstract fun tagsDao(): TagsDao
|
||||
}
|
||||
14
app/src/main/java/org/koitharu/kotatsu/core/db/TagsDao.kt
Normal file
14
app/src/main/java/org/koitharu/kotatsu/core/db/TagsDao.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
@Dao
|
||||
interface TagsDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM tags")
|
||||
fun getAllTags(): List<TagEntity>
|
||||
}
|
||||
@@ -1,9 +1,25 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "history")
|
||||
data class HistoryEntity(
|
||||
@PrimaryKey val id: Long
|
||||
)
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "updated_at") val updatedAt: Long,
|
||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||
@ColumnInfo(name = "page") val page: Int
|
||||
) {
|
||||
|
||||
fun toMangaHistory() = MangaHistory(
|
||||
createdAt = Date(createdAt),
|
||||
updatedAt = Date(updatedAt),
|
||||
chapterId = chapterId,
|
||||
page = page
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
|
||||
data class HistoryWithManga(
|
||||
@Embedded val history: HistoryEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "manga_id"
|
||||
)
|
||||
val manga: MangaEntity
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaState
|
||||
|
||||
@Entity(tableName = "manga")
|
||||
data class MangaEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "localized_title") val localizedTitle: String? = null,
|
||||
@ColumnInfo(name = "url") val url: String,
|
||||
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
|
||||
@ColumnInfo(name = "cover_url") val coverUrl: String,
|
||||
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
|
||||
@ColumnInfo(name = "summary") val summary: String,
|
||||
@ColumnInfo(name = "state") val state: String? = null,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
) {
|
||||
|
||||
fun toManga() = Manga(
|
||||
id = this.id,
|
||||
title = this.title,
|
||||
localizedTitle = this.localizedTitle,
|
||||
summary = this.summary,
|
||||
state = this.state?.let { MangaState.valueOf(it) },
|
||||
rating = this.rating,
|
||||
url = this.url,
|
||||
coverUrl = this.coverUrl,
|
||||
largeCoverUrl = this.largeCoverUrl,
|
||||
source = MangaSource.valueOf(this.source)
|
||||
// tags = this.tags.map(TagEntity::toMangaTag).toSet()
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(manga: Manga) = MangaEntity(
|
||||
id = manga.id,
|
||||
url = manga.url,
|
||||
source = manga.source.name,
|
||||
largeCoverUrl = manga.largeCoverUrl,
|
||||
coverUrl = manga.coverUrl,
|
||||
localizedTitle = manga.localizedTitle,
|
||||
rating = manga.rating,
|
||||
state = manga.state?.name,
|
||||
summary = manga.summary,
|
||||
// tags = manga.tags.map(TagEntity.Companion::fromMangaTag),
|
||||
title = manga.title
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"])
|
||||
data class MangaTagsEntity(
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "tag_id") val tagId: Long
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
|
||||
@Entity(tableName = "tags")
|
||||
data class TagEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "tag_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "key") val key: String,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
) {
|
||||
|
||||
fun toMangaTag() = MangaTag(
|
||||
key = this.key,
|
||||
title = this.title,
|
||||
source = MangaSource.valueOf(this.source)
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromMangaTag(tag: MangaTag) = TagEntity(
|
||||
title = tag.title,
|
||||
key = tag.key,
|
||||
source = tag.source.name,
|
||||
id = "${tag.key}_${tag.source.name}".longHashCode()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
data class MangaHistory(
|
||||
val createdAt: Date,
|
||||
val updatedAt: Date,
|
||||
val chapterId: Long,
|
||||
val page: Int
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
data class MangaInfo <E>(
|
||||
val manga: Manga,
|
||||
val extra: E
|
||||
)
|
||||
@@ -4,7 +4,6 @@ import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import org.koitharu.kotatsu.domain.MangaRepository
|
||||
import org.koitharu.kotatsu.domain.repository.ReadmangaRepository
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Parcelize
|
||||
enum class MangaSource(val title: String, val cls: Class<out MangaRepository>): Parcelable {
|
||||
|
||||
@@ -6,5 +6,6 @@ import kotlinx.android.parcel.Parcelize
|
||||
@Parcelize
|
||||
data class MangaTag(
|
||||
val title: String,
|
||||
val key: String
|
||||
val key: String,
|
||||
val source: MangaSource
|
||||
) : Parcelable
|
||||
@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.core.prefs
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.delegates.prefs.EnumPreferenceDelegate
|
||||
@@ -13,7 +11,7 @@ class AppSettings private constructor(private val resources: Resources, private
|
||||
|
||||
constructor(context: Context) : this(context.resources, PreferenceManager.getDefaultSharedPreferences(context))
|
||||
|
||||
var listMode by EnumPreferenceDelegate(ListMode::class.java, resources.getString(R.string.key_list_mode), ListMode.LIST)
|
||||
var listMode by EnumPreferenceDelegate(ListMode::class.java, resources.getString(R.string.key_list_mode), ListMode.DETAILED_LIST)
|
||||
|
||||
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
|
||||
abstract class BaseMangaRepository(protected val loaderContext: MangaLoaderContext) : MangaRepository {
|
||||
|
||||
override val sortOrders: Set<SortOrder> get() = emptySet()
|
||||
|
||||
override val isSearchAvailable get() = true
|
||||
|
||||
override suspend fun getPageFullUrl(page: MangaPage) : String = page.url
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import java.io.Closeable
|
||||
|
||||
class HistoryRepository() : KoinComponent, MangaRepository, Closeable {
|
||||
|
||||
private val db: MangaDatabase by inject()
|
||||
|
||||
override val sortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST, SortOrder.POPULARITY)
|
||||
|
||||
override val isSearchAvailable = false
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
sortOrder: SortOrder?,
|
||||
tags: Set<String>?
|
||||
): List<Manga> = getHistory(offset, query, sortOrder, tags).map { x -> x.manga }
|
||||
|
||||
suspend fun getHistory(
|
||||
offset: Int,
|
||||
query: String? = null,
|
||||
sortOrder: SortOrder? = null,
|
||||
tags: Set<String>? = null
|
||||
): List<MangaInfo<MangaHistory>> {
|
||||
val entities = db.historyDao().getAll(offset, 20, "updated_by")
|
||||
return entities.map { x -> MangaInfo(x.manga.toManga(), x.history.toMangaHistory()) }
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
throw UnsupportedOperationException("History repository does not support getDetails() method")
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
throw UnsupportedOperationException("History repository does not support getPages() method")
|
||||
}
|
||||
|
||||
override suspend fun getPageFullUrl(page: MangaPage) = page.url
|
||||
|
||||
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int) {
|
||||
val dao = db.historyDao()
|
||||
val entity = HistoryEntity(
|
||||
mangaId = manga.id,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
chapterId = chapterId,
|
||||
page = page
|
||||
)
|
||||
dao.upsert(
|
||||
HistoryWithManga(
|
||||
history = entity,
|
||||
manga = MangaEntity.from(manga)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun clear() {
|
||||
db.historyDao().clear()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
|
||||
abstract class MangaRepository(protected val loaderContext: MangaLoaderContext) {
|
||||
interface MangaRepository {
|
||||
|
||||
open val sortOrders: Set<SortOrder> get() = emptySet()
|
||||
val sortOrders: Set<SortOrder>
|
||||
|
||||
open val isSearchAvailable get() = true
|
||||
val isSearchAvailable: Boolean
|
||||
|
||||
abstract suspend fun getList(offset: Int, query: String? = null, sortOrder: SortOrder? = null, tags: Set<String>? = null): List<Manga>
|
||||
suspend fun getList(offset: Int, query: String? = null, sortOrder: SortOrder? = null, tags: Set<String>? = null): List<Manga>
|
||||
|
||||
abstract suspend fun getDetails(manga: Manga) : Manga
|
||||
suspend fun getDetails(manga: Manga) : Manga
|
||||
|
||||
abstract suspend fun getPages(chapter: MangaChapter) : List<MangaPage>
|
||||
suspend fun getPages(chapter: MangaChapter) : List<MangaPage>
|
||||
|
||||
open suspend fun getPageFullUrl(page: MangaPage) : String = page.url
|
||||
suspend fun getPageFullUrl(page: MangaPage) : String
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
package org.koitharu.kotatsu.domain.repository
|
||||
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import org.koitharu.kotatsu.core.model.*
|
||||
import org.koitharu.kotatsu.domain.BaseMangaRepository
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.domain.MangaRepository
|
||||
import org.koitharu.kotatsu.domain.exceptions.ParseException
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(loaderContext) {
|
||||
class ReadmangaRepository(loaderContext: MangaLoaderContext) : BaseMangaRepository(loaderContext) {
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
@@ -47,7 +46,8 @@ class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(l
|
||||
?.map {
|
||||
MangaTag(
|
||||
title = it.text(),
|
||||
key = it.attr("href").substringAfterLast('/')
|
||||
key = it.attr("href").substringAfterLast('/'),
|
||||
source = MangaSource.READMANGA_RU
|
||||
)
|
||||
}?.toSet()
|
||||
}.orEmpty(),
|
||||
|
||||
@@ -10,7 +10,8 @@ import kotlinx.android.synthetic.main.activity_main.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.ui.common.BaseActivity
|
||||
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
|
||||
import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment
|
||||
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
|
||||
|
||||
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
@@ -29,7 +30,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
navigationView.setNavigationItemSelectedListener(this)
|
||||
|
||||
if (!supportFragmentManager.isStateSaved) {
|
||||
setPrimaryFragment(MangaListFragment.newInstance(MangaSource.READMANGA_RU))
|
||||
setPrimaryFragment(RemoteListFragment.newInstance(MangaSource.READMANGA_RU))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +52,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
if (item.groupId == R.id.group_remote_sources) {
|
||||
val source = MangaSource.values().getOrNull(item.itemId) ?: return false
|
||||
setPrimaryFragment(MangaListFragment.newInstance(source))
|
||||
setPrimaryFragment(RemoteListFragment.newInstance(source))
|
||||
} else when (item.itemId) {
|
||||
R.id.nav_history -> Unit
|
||||
R.id.nav_history -> setPrimaryFragment(HistoryListFragment.newInstance())
|
||||
R.id.nav_favourites -> Unit
|
||||
R.id.nav_local_storage -> Unit
|
||||
else -> return false
|
||||
|
||||
@@ -46,6 +46,8 @@ class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), Vie
|
||||
|
||||
private const val TAG = "ListModeSelectDialog"
|
||||
|
||||
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
|
||||
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm,
|
||||
TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,17 @@ import coil.api.load
|
||||
import coil.request.RequestDisposable
|
||||
import kotlinx.android.synthetic.main.item_manga_grid.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaInfo
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
|
||||
class MangaGridHolder(parent: ViewGroup) : BaseViewHolder<Manga>(parent, R.layout.item_manga_grid) {
|
||||
class MangaGridHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_grid) {
|
||||
|
||||
private var coverRequest: RequestDisposable? = null
|
||||
|
||||
override fun onBind(data: Manga) {
|
||||
override fun onBind(data: MangaInfo<E>) {
|
||||
coverRequest?.dispose()
|
||||
textView_title.text = data.title
|
||||
coverRequest = imageView_cover.load(data.coverUrl) {
|
||||
textView_title.text = data.manga.title
|
||||
coverRequest = imageView_cover.load(data.manga.coverUrl) {
|
||||
crossfade(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package org.koitharu.kotatsu.ui.main.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaInfo
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
|
||||
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
|
||||
|
||||
class MangaListAdapter(onItemClickListener: OnRecyclerItemClickListener<Manga>) :
|
||||
BaseRecyclerAdapter<Manga>(onItemClickListener) {
|
||||
class MangaListAdapter<E>(onItemClickListener: OnRecyclerItemClickListener<MangaInfo<E>>) :
|
||||
BaseRecyclerAdapter<MangaInfo<E>>(onItemClickListener) {
|
||||
|
||||
var listMode: ListMode = ListMode.LIST
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = when(listMode) {
|
||||
ListMode.LIST -> MangaListHolder(parent)
|
||||
ListMode.DETAILED_LIST -> MangaListDetailsHolder(parent)
|
||||
ListMode.LIST -> MangaListHolder<E>(parent)
|
||||
ListMode.DETAILED_LIST -> MangaListDetailsHolder<E>(parent)
|
||||
ListMode.GRID -> MangaGridHolder(parent)
|
||||
}
|
||||
|
||||
override fun onGetItemId(item: Manga) = item.id
|
||||
override fun onGetItemId(item: MangaInfo<E>) = item.manga.id
|
||||
}
|
||||
@@ -8,29 +8,30 @@ import coil.request.RequestDisposable
|
||||
import kotlinx.android.synthetic.main.item_manga_list_details.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaInfo
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MangaListDetailsHolder(parent: ViewGroup) : BaseViewHolder<Manga>(parent, R.layout.item_manga_list_details) {
|
||||
class MangaListDetailsHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_list_details) {
|
||||
|
||||
private var coverRequest: RequestDisposable? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBind(data: Manga) {
|
||||
override fun onBind(data: MangaInfo<E>) {
|
||||
coverRequest?.dispose()
|
||||
textView_title.text = data.title
|
||||
textView_subtitle.textAndVisible = data.localizedTitle
|
||||
coverRequest = imageView_cover.load(data.coverUrl) {
|
||||
textView_title.text = data.manga.title
|
||||
textView_subtitle.textAndVisible = data.manga.localizedTitle
|
||||
coverRequest = imageView_cover.load(data.manga.coverUrl) {
|
||||
crossfade(true)
|
||||
}
|
||||
if(data.rating == Manga.NO_RATING) {
|
||||
if(data.manga.rating == Manga.NO_RATING) {
|
||||
textView_rating.isVisible = false
|
||||
} else {
|
||||
textView_rating.text = "${(data.rating * 10).roundToInt()}/10"
|
||||
textView_rating.text = "${(data.manga.rating * 10).roundToInt()}/10"
|
||||
textView_rating.isVisible = true
|
||||
}
|
||||
textView_tags.text = data.tags.joinToString(", ") {
|
||||
textView_tags.text = data.manga.tags.joinToString(", ") {
|
||||
it.title
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,26 +13,23 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.fragment_list.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaInfo
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
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.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
||||
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.hasItems
|
||||
|
||||
class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
|
||||
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga> {
|
||||
abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), MangaListView<E>,
|
||||
PaginationScrollListener.Callback, OnRecyclerItemClickListener<MangaInfo<E>> {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::MangaListPresenter)
|
||||
|
||||
private val source by arg<MangaSource>(ARG_SOURCE)
|
||||
|
||||
private lateinit var adapter: MangaListAdapter
|
||||
private lateinit var adapter: MangaListAdapter<E>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -46,7 +43,7 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
presenter.loadList(source, 0)
|
||||
onRequestMoreItems(0)
|
||||
}
|
||||
settings.subscribe(this)
|
||||
}
|
||||
@@ -58,7 +55,9 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
presenter.loadList(source, 0)
|
||||
if (!recyclerView.hasItems) {
|
||||
onRequestMoreItems(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@@ -74,19 +73,16 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Manga, position: Int, view: View) {
|
||||
startActivity(MangaDetailsActivity.newIntent(context ?: return, item))
|
||||
override fun onItemClick(item: MangaInfo<E>, position: Int, view: View) {
|
||||
startActivity(MangaDetailsActivity.newIntent(context ?: return, item.manga))
|
||||
}
|
||||
|
||||
override fun onRequestMoreItems(offset: Int) {
|
||||
presenter.loadList(source, offset)
|
||||
}
|
||||
|
||||
override fun onListChanged(list: List<Manga>) {
|
||||
override fun onListChanged(list: List<MangaInfo<E>>) {
|
||||
adapter.replaceData(list)
|
||||
layout_holder.isVisible = list.isEmpty()
|
||||
}
|
||||
|
||||
override fun onListAppended(list: List<Manga>) {
|
||||
override fun onListAppended(list: List<MangaInfo<E>>) {
|
||||
adapter.appendData(list)
|
||||
}
|
||||
|
||||
@@ -101,10 +97,9 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
|
||||
progressBar.isVisible = isLoading && !hasItems
|
||||
swipeRefreshLayout.isRefreshing = isLoading && hasItems
|
||||
swipeRefreshLayout.isEnabled = !progressBar.isVisible
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return source.title
|
||||
if (isLoading) {
|
||||
layout_holder.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
@@ -133,13 +128,4 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
|
||||
adapter.notifyDataSetChanged()
|
||||
recyclerView.firstItem = position
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_SOURCE = "provider"
|
||||
|
||||
fun newInstance(provider: MangaSource) = MangaListFragment().withArgs(1) {
|
||||
putParcelable(ARG_SOURCE, provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,19 @@ import coil.api.load
|
||||
import coil.request.RequestDisposable
|
||||
import kotlinx.android.synthetic.main.item_manga_list.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaInfo
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
|
||||
class MangaListHolder(parent: ViewGroup) : BaseViewHolder<Manga>(parent, R.layout.item_manga_list) {
|
||||
class MangaListHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_list) {
|
||||
|
||||
private var coverRequest: RequestDisposable? = null
|
||||
|
||||
override fun onBind(data: Manga) {
|
||||
override fun onBind(data: MangaInfo<E>) {
|
||||
coverRequest?.dispose()
|
||||
textView_title.text = data.title
|
||||
textView_subtitle.textAndVisible = data.localizedTitle
|
||||
coverRequest = imageView_cover.load(data.coverUrl) {
|
||||
textView_title.text = data.manga.title
|
||||
textView_subtitle.textAndVisible = data.manga.localizedTitle
|
||||
coverRequest = imageView_cover.load(data.manga.coverUrl) {
|
||||
crossfade(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ package org.koitharu.kotatsu.ui.main.list
|
||||
|
||||
import moxy.MvpView
|
||||
import moxy.viewstate.strategy.*
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaInfo
|
||||
|
||||
interface MangaListView : MvpView {
|
||||
interface MangaListView<E> : MvpView {
|
||||
|
||||
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
|
||||
fun onListChanged(list: List<Manga>)
|
||||
fun onListChanged(list: List<MangaInfo<E>>)
|
||||
|
||||
@StateStrategyType(AddToEndStrategy::class, tag = "content")
|
||||
fun onListAppended(list: List<Manga>)
|
||||
fun onListAppended(list: List<MangaInfo<E>>)
|
||||
|
||||
@StateStrategyType(AddToEndSingleStrategy::class)
|
||||
fun onLoadingChanged(isLoading: Boolean)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.koitharu.kotatsu.ui.main.list.history
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.fragment_list.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
|
||||
import org.koitharu.kotatsu.ui.main.list.MangaListView
|
||||
|
||||
class HistoryListFragment : MangaListFragment<MangaHistory>(), MangaListView<MangaHistory>{
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::HistoryListPresenter)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
textView_holder.setText(R.string.history_is_empty)
|
||||
}
|
||||
|
||||
override fun onRequestMoreItems(offset: Int) {
|
||||
presenter.loadList(offset)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.opt_history, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
presenter.clearHistory()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return getString(R.string.history)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = HistoryListFragment()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.koitharu.kotatsu.ui.main.list.history
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moxy.InjectViewState
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.ui.common.BasePresenter
|
||||
import org.koitharu.kotatsu.ui.main.list.MangaListView
|
||||
|
||||
@InjectViewState
|
||||
class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() {
|
||||
|
||||
private lateinit var repository: HistoryRepository
|
||||
|
||||
override fun onFirstViewAttach() {
|
||||
repository = HistoryRepository()
|
||||
super.onFirstViewAttach()
|
||||
}
|
||||
|
||||
fun loadList(offset: Int) {
|
||||
launch {
|
||||
viewState.onLoadingChanged(true)
|
||||
try {
|
||||
val list = withContext(Dispatchers.IO) {
|
||||
repository.getHistory(offset = offset)
|
||||
}
|
||||
if (offset == 0) {
|
||||
viewState.onListChanged(list)
|
||||
} else {
|
||||
viewState.onListAppended(list)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
viewState.onError(e)
|
||||
} finally {
|
||||
viewState.onLoadingChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearHistory() {
|
||||
launch {
|
||||
viewState.onLoadingChanged(true)
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
repository.clear()
|
||||
}
|
||||
viewState.onListChanged(emptyList())
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
viewState.onError(e)
|
||||
} finally {
|
||||
viewState.onLoadingChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
repository.closeQuietly()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.koitharu.kotatsu.ui.main.list.remote
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.fragment_list.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class RemoteListFragment : MangaListFragment<Unit>() {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ::RemoteListPresenter)
|
||||
|
||||
private val source by arg<MangaSource>(ARG_SOURCE)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
textView_holder.setText(R.string.nothing_found)
|
||||
}
|
||||
|
||||
override fun onRequestMoreItems(offset: Int) {
|
||||
presenter.loadList(source, offset)
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return source.title
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_SOURCE = "provider"
|
||||
|
||||
fun newInstance(provider: MangaSource) = RemoteListFragment().withArgs(1) {
|
||||
putParcelable(ARG_SOURCE, provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
package org.koitharu.kotatsu.ui.main.list
|
||||
package org.koitharu.kotatsu.ui.main.list.remote
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moxy.InjectViewState
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.MangaInfo
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||
import org.koitharu.kotatsu.ui.common.BasePresenter
|
||||
import org.koitharu.kotatsu.ui.main.list.MangaListView
|
||||
|
||||
@InjectViewState
|
||||
class MangaListPresenter : BasePresenter<MangaListView>() {
|
||||
class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() {
|
||||
|
||||
fun loadList(source: MangaSource, offset: Int) {
|
||||
launch {
|
||||
@@ -19,6 +21,7 @@ class MangaListPresenter : BasePresenter<MangaListView>() {
|
||||
val list = withContext(Dispatchers.IO) {
|
||||
MangaProviderFactory.create(source)
|
||||
.getList(offset)
|
||||
.map { MangaInfo(it, Unit) }
|
||||
}
|
||||
if (offset == 0) {
|
||||
viewState.onListChanged(list)
|
||||
@@ -6,12 +6,10 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.common.BaseActivity
|
||||
import org.koitharu.kotatsu.utils.ext.showDialog
|
||||
@@ -58,6 +56,11 @@ class ReaderActivity : BaseActivity(), ReaderView {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
presenter.addToHistory(manga, chapterId, pager.currentItem)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.opt_reader_top, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
|
||||
@@ -5,12 +5,14 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moxy.InjectViewState
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||
import org.koitharu.kotatsu.ui.common.BasePresenter
|
||||
|
||||
@InjectViewState
|
||||
class ReaderPresenter() : BasePresenter<ReaderView>() {
|
||||
class ReaderPresenter : BasePresenter<ReaderView>() {
|
||||
|
||||
fun loadChapter(chapter: MangaChapter) {
|
||||
launch {
|
||||
@@ -31,4 +33,12 @@ class ReaderPresenter() : BasePresenter<ReaderView>() {
|
||||
}
|
||||
}
|
||||
|
||||
fun addToHistory(manga: Manga, chapterId: Long, page: Int) {
|
||||
launch(Dispatchers.IO) {
|
||||
HistoryRepository().use {
|
||||
it.addOrUpdate(manga, chapterId, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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"
|
||||
@@ -26,6 +27,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
@@ -34,7 +37,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
android:text="?android:textColorSecondary"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
12
app/src/main/res/menu/opt_history.xml
Normal file
12
app/src/main/res/menu/opt_history.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_clear_history"
|
||||
android:orderInCategory="50"
|
||||
android:title="@string/clear_history"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
@@ -19,4 +19,7 @@
|
||||
<string name="chapter_d_of_d">Chapter %d of %d</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<string name="clear_history">Clear history</string>
|
||||
<string name="nothing_found">Nothing found</string>
|
||||
<string name="history_is_empty">History is empty</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user